This is an automated email from the ASF dual-hosted git repository. kenhuuu pushed a commit to branch stringify-params in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit b5423bb36ea630681209bb50b584e7d9b299cbd2 Author: Ken Hu <[email protected]> AuthorDate: Thu Apr 23 19:51:17 2026 -0700 change to seconds and nanos --- .../language/grammar/GenericLiteralVisitor.java | 32 ++-- .../translator/DotNetTranslateVisitor.java | 19 ++- .../language/translator/GoTranslateVisitor.java | 10 +- .../translator/GroovyTranslateVisitor.java | 10 +- .../language/translator/JavaTranslateVisitor.java | 10 +- .../translator/PythonTranslateVisitor.java | 19 ++- .../gremlin/process/traversal/GremlinLang.java | 13 +- .../grammar/GeneralLiteralVisitorTest.java | 72 +++------ .../language/translator/GremlinTranslatorTest.java | 38 +++-- .../gremlin/process/traversal/GremlinLangTest.java | 5 - gremlin-dotnet/build/generate.groovy | 1 - .../Gremlin.Net/Process/Traversal/GremlinLang.cs | 12 +- .../Gherkin/CommonSteps.cs | 21 ++- .../Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs | 20 +-- .../Process/Traversal/GremlinLangTests.cs | 23 --- gremlin-go/driver/cucumber/cucumberSteps_test.go | 49 +++--- gremlin-go/driver/cucumber/gremlin.go | 17 +-- gremlin-go/driver/gremlinlang.go | 60 ++------ gremlin-go/driver/gremlinlang_test.go | 22 --- .../language/translator/DotNetTranslateVisitor.ts | 77 ++-------- .../lib/language/translator/GoTranslateVisitor.ts | 42 +---- .../language/translator/GroovyTranslateVisitor.ts | 11 +- .../language/translator/JavaTranslateVisitor.ts | 11 +- .../language/translator/PythonTranslateVisitor.ts | 56 ++----- .../gremlin-javascript/test/cucumber/gremlin.js | 17 +-- gremlin-language/src/main/antlr4/Gremlin.g4 | 2 +- .../python/gremlin_python/process/traversal.py | 48 ++---- .../src/main/python/tests/feature/feature_steps.py | 21 ++- .../src/main/python/tests/feature/gremlin.py | 17 +-- .../python/tests/unit/process/test_gremlin_lang.py | 8 - .../tinkerpop/gremlin/features/StepDefinition.java | 17 ++- .../gremlin/language/translator/translations.json | 170 ++++++++------------- .../gremlin/test/features/data/Duration.feature | 82 +++------- 33 files changed, 356 insertions(+), 676 deletions(-) diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GenericLiteralVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GenericLiteralVisitor.java index d6f76e2738..6a7f621c10 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GenericLiteralVisitor.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GenericLiteralVisitor.java @@ -34,7 +34,6 @@ import java.math.BigInteger; import java.nio.ByteBuffer; import java.time.Duration; import java.time.OffsetDateTime; -import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.Base64; import java.util.HashSet; @@ -542,23 +541,26 @@ public class GenericLiteralVisitor extends DefaultGremlinBaseVisitor<Object> { */ @Override public Object visitDurationLiteral(final GremlinParser.DurationLiteralContext ctx) { - final String iso8601 = (String) antlr.genericVisitor.visitStringLiteral(ctx.stringLiteral()); - - // reject year/month components - check for Y or M before the T separator - final String upper = iso8601.toUpperCase(); - final int tIndex = upper.indexOf('T'); - final String datePart = tIndex >= 0 ? upper.substring(0, tIndex) : upper; - if (datePart.contains("Y") || datePart.contains("M")) { - throw new GremlinParserException(String.format( - "Duration literal does not support year or month components: '%s'. Use days, hours, minutes, and seconds only.", iso8601)); - } + final Number secondsNum = (Number) antlr.genericVisitor.visitIntegerLiteral(ctx.integerLiteral(0)); + final Number nanosNum = (Number) antlr.genericVisitor.visitIntegerLiteral(ctx.integerLiteral(1)); - try { - return Duration.parse(iso8601); - } catch (DateTimeParseException e) { + final long seconds = secondsNum.longValue(); + final long nanos = nanosNum.longValue(); + + if (seconds < 0) { throw new GremlinParserException( - String.format("Invalid Duration literal: '%s' is not a valid ISO-8601 duration", iso8601)); + String.format("Duration seconds must be non-negative, got: %d", seconds)); } + if (nanos < 0 || nanos > 999999999) { + throw new GremlinParserException( + String.format("Duration nanoseconds must be between 0 and 999999999, got: %d", nanos)); + } + + final boolean isPositive = ctx.booleanLiteral() == null || + Boolean.parseBoolean(ctx.booleanLiteral().getText()); + + final Duration d = Duration.ofSeconds(seconds, nanos); + return isPositive ? d : d.negated(); } /** diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/DotNetTranslateVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/DotNetTranslateVisitor.java index 663547b520..11546c6ca2 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/DotNetTranslateVisitor.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/DotNetTranslateVisitor.java @@ -1195,16 +1195,15 @@ public class DotNetTranslateVisitor extends AbstractTranslateVisitor { @Override public Void visitDurationLiteral(final GremlinParser.DurationLiteralContext ctx) { - final String iso8601 = removeFirstAndLastCharacters(ctx.stringLiteral().getText()); - // .NET's XmlConvert.ToTimeSpan requires "-PT30S" not "PT-30S", so parse and - // re-emit in the normalized form that .NET expects - final java.time.Duration d = java.time.Duration.parse(iso8601); - final String normalized = d.isNegative() - ? "-" + d.negated().toString() - : d.toString(); - sb.append("XmlConvert.ToTimeSpan(\""); - sb.append(normalized); - sb.append("\")"); + final long seconds = Long.parseLong(ctx.integerLiteral(0).getText()); + final int nanos = Integer.parseInt(ctx.integerLiteral(1).getText()); + final boolean isPositive = ctx.booleanLiteral() == null || + Boolean.parseBoolean(ctx.booleanLiteral().getText()); + // Convert to ticks: 1 tick = 100 nanoseconds, 1 second = 10,000,000 ticks + final long ticks = seconds * 10_000_000L + nanos / 100; + sb.append(String.format("TimeSpan.FromTicks(%dL)", ticks)); + if (!isPositive) + sb.append(".Negate()"); return null; } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GoTranslateVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GoTranslateVisitor.java index 7a81b84a72..f9c30a017f 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GoTranslateVisitor.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GoTranslateVisitor.java @@ -28,7 +28,6 @@ import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.Option import org.apache.tinkerpop.gremlin.structure.VertexProperty; import org.apache.tinkerpop.gremlin.util.DatetimeHelper; -import java.time.Duration; import java.time.OffsetDateTime; import java.util.Arrays; import java.util.Base64; @@ -355,9 +354,12 @@ public class GoTranslateVisitor extends AbstractTranslateVisitor { @Override public Void visitDurationLiteral(final GremlinParser.DurationLiteralContext ctx) { - final String iso8601 = removeFirstAndLastCharacters(ctx.stringLiteral().getText()); - final Duration d = Duration.parse(iso8601); - sb.append("time.Duration(").append(d.toNanos()).append(")"); + final long seconds = Long.parseLong(ctx.integerLiteral(0).getText()); + final int nanos = Integer.parseInt(ctx.integerLiteral(1).getText()); + final boolean isPositive = ctx.booleanLiteral() == null || + Boolean.parseBoolean(ctx.booleanLiteral().getText()); + final long totalNanos = seconds * 1_000_000_000L + nanos; + sb.append("time.Duration(").append(isPositive ? totalNanos : -totalNanos).append(")"); return null; } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GroovyTranslateVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GroovyTranslateVisitor.java index a6e1f6050a..009233c9eb 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GroovyTranslateVisitor.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/GroovyTranslateVisitor.java @@ -142,9 +142,13 @@ public class GroovyTranslateVisitor extends TranslateVisitor { @Override public Void visitDurationLiteral(final GremlinParser.DurationLiteralContext ctx) { - sb.append("Duration.parse("); - sb.append(ctx.stringLiteral().getText()); - sb.append(")"); + final long seconds = Long.parseLong(ctx.integerLiteral(0).getText()); + final int nanos = Integer.parseInt(ctx.integerLiteral(1).getText()); + final boolean isPositive = ctx.booleanLiteral() == null || + Boolean.parseBoolean(ctx.booleanLiteral().getText()); + sb.append(String.format("Duration.ofSeconds(%d, %d)", seconds, nanos)); + if (!isPositive) + sb.append(".negated()"); return null; } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/JavaTranslateVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/JavaTranslateVisitor.java index 9a0c7dbdb5..ee03430118 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/JavaTranslateVisitor.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/JavaTranslateVisitor.java @@ -260,9 +260,13 @@ public class JavaTranslateVisitor extends AbstractTranslateVisitor { @Override public Void visitDurationLiteral(final GremlinParser.DurationLiteralContext ctx) { - sb.append("Duration.parse("); - sb.append(ctx.stringLiteral().getText()); - sb.append(")"); + final long seconds = Long.parseLong(ctx.integerLiteral(0).getText()); + final int nanos = Integer.parseInt(ctx.integerLiteral(1).getText()); + final boolean isPositive = ctx.booleanLiteral() == null || + Boolean.parseBoolean(ctx.booleanLiteral().getText()); + sb.append(String.format("Duration.ofSeconds(%d, %d)", seconds, nanos)); + if (!isPositive) + sb.append(".negated()"); return null; } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/PythonTranslateVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/PythonTranslateVisitor.java index 9a9699de88..e6297dc539 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/PythonTranslateVisitor.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/translator/PythonTranslateVisitor.java @@ -26,7 +26,6 @@ import org.apache.tinkerpop.gremlin.structure.VertexProperty; import org.apache.tinkerpop.gremlin.util.DatetimeHelper; import java.math.BigInteger; -import java.time.Duration; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -301,15 +300,19 @@ public class PythonTranslateVisitor extends AbstractTranslateVisitor { @Override public Void visitDurationLiteral(final GremlinParser.DurationLiteralContext ctx) { - final String iso8601 = removeFirstAndLastCharacters(ctx.stringLiteral().getText()); - final Duration d = Duration.parse(iso8601); - final long totalSeconds = d.getSeconds(); - final int nanos = d.getNano(); - if (nanos == 0) { + final long seconds = Long.parseLong(ctx.integerLiteral(0).getText()); + final int nanos = Integer.parseInt(ctx.integerLiteral(1).getText()); + final boolean isPositive = ctx.booleanLiteral() == null || + Boolean.parseBoolean(ctx.booleanLiteral().getText()); + final long totalSeconds = isPositive ? seconds : -seconds; + // Python's timedelta has microsecond resolution; sub-microsecond nanos are truncated + final long micros = nanos / 1000; + if (micros == 0) { sb.append("timedelta(seconds=").append(totalSeconds).append(")"); } else { - final long micros = nanos / 1000; - sb.append("timedelta(seconds=").append(totalSeconds).append(",microseconds=").append(micros).append(")"); + // for negative durations, microseconds must also be negated + final long totalMicros = isPositive ? micros : -micros; + sb.append("timedelta(seconds=").append(totalSeconds).append(",microseconds=").append(totalMicros).append(")"); } return null; } diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java index d895f6ec5b..aa00fccb8c 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java @@ -155,8 +155,17 @@ public class GremlinLang implements Cloneable, Serializable { // is pure ASCII and avoids encoding issues across different platforms return String.format("\"%s\"c", StringEscapeUtils.escapeJava(arg.toString())); - if (arg instanceof Duration) - return String.format("Duration(\"%s\")", arg); + if (arg instanceof Duration) { + final Duration d = (Duration) arg; + final boolean isNegative = d.isNegative(); + // negate to get magnitude components - Java's Duration stores negative values + // with adjusted seconds (e.g. -1.5s is seconds=-2, nanos=500000000) + final Duration abs = isNegative ? d.negated() : d; + if (isNegative) + return String.format("Duration(%d,%d,false)", abs.getSeconds(), abs.getNano()); + else + return String.format("Duration(%d,%d)", abs.getSeconds(), abs.getNano()); + } if (arg instanceof ByteBuffer) { // duplicate() shares the underlying data but gives an independent position cursor, diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/GeneralLiteralVisitorTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/GeneralLiteralVisitorTest.java index 7dd40a61fb..ed519b6b31 100644 --- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/GeneralLiteralVisitorTest.java +++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/GeneralLiteralVisitorTest.java @@ -941,22 +941,17 @@ public class GeneralLiteralVisitorTest { @Parameterized.Parameters(name = "{0}") public static Iterable<Object[]> generateTestParameters() { return Arrays.asList(new Object[][]{ - {"Duration(\"PT2H30M\")", Duration.ofHours(2).plusMinutes(30)}, - {"Duration(\"PT0S\")", Duration.ZERO}, - {"Duration(\"P1DT12H\")", Duration.ofDays(1).plusHours(12)}, - {"Duration(\"PT-30S\")", Duration.ofSeconds(-30)}, - {"Duration(\"PT0.5S\")", Duration.ofMillis(500)}, - {"Duration(\"-PT30S\")", Duration.ofSeconds(-30)}, - {"Duration(\"-P1DT12H\")", Duration.ofHours(-36)}, - {"Duration(\"P2D\")", Duration.ofDays(2)}, - {"Duration(\"PT1H30M15S\")", Duration.ofHours(1).plusMinutes(30).plusSeconds(15)}, - {"Duration(\"P0D\")", Duration.ZERO}, - {"Duration(\"PT0.000000001S\")", Duration.ofNanos(1)}, - {"Duration(\"PT36H\")", Duration.ofHours(36)}, - {"Duration(\"PT0.0000001S\")", Duration.ofNanos(100)}, - {"Duration(\"PT0.000001S\")", Duration.ofNanos(1000)}, - {"Duration(\"PT-0.5S\")", Duration.ofMillis(-500)}, - {"Duration(\"-PT0.5S\")", Duration.ofMillis(-500)}, + {"Duration(9000, 0)", Duration.ofHours(2).plusMinutes(30)}, + {"Duration(0, 0)", Duration.ZERO}, + {"Duration(30, 0)", Duration.ofSeconds(30)}, + {"Duration(0, 500000000)", Duration.ofMillis(500)}, + {"Duration(5415, 0)", Duration.ofHours(1).plusMinutes(30).plusSeconds(15)}, + {"Duration(0, 1)", Duration.ofNanos(1)}, + {"Duration(1, 500000000)", Duration.ofSeconds(1).plusMillis(500)}, + {"Duration(30, 0, false)", Duration.ofSeconds(-30)}, + {"Duration(0, 500000000, false)", Duration.ofMillis(-500)}, + {"Duration(0, 0, false)", Duration.ZERO}, + {"Duration(0, 0, true)", Duration.ZERO}, }); } @@ -971,62 +966,41 @@ public class GeneralLiteralVisitorTest { public static class InvalidDurationLiteralTest { @Test - public void shouldFailOnInvalidDuration() { - final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString("Duration(\"not-a-duration\")")); + public void shouldFailOnNegativeNanos() { + final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString("Duration(0, -1)")); final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer)); final GremlinParser.DurationLiteralContext ctx = parser.durationLiteral(); try { new GenericLiteralVisitor(new GremlinAntlrToJava()).visitDurationLiteral(ctx); - fail("Invalid Duration value should have thrown exception"); + fail("Negative nanos should have thrown exception"); } catch (GremlinParserException gpe) { - assertThat(gpe.getMessage().contains("Invalid Duration literal:"), Matchers.is(true)); - } - } - - @Test(expected = GremlinParserException.class) - public void shouldFailOnEmptyDuration() { - final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString("Duration(\"\")")); - final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer)); - final GremlinParser.DurationLiteralContext ctx = parser.durationLiteral(); - new GenericLiteralVisitor(new GremlinAntlrToJava()).visitDurationLiteral(ctx); - } - - @Test - public void shouldFailOnYearComponent() { - final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString("Duration(\"P1Y\")")); - final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer)); - final GremlinParser.DurationLiteralContext ctx = parser.durationLiteral(); - try { - new GenericLiteralVisitor(new GremlinAntlrToJava()).visitDurationLiteral(ctx); - fail("Year component should have thrown exception"); - } catch (GremlinParserException gpe) { - assertThat(gpe.getMessage().contains("does not support year or month components"), Matchers.is(true)); + assertThat(gpe.getMessage().contains("nanoseconds must be between 0 and 999999999"), Matchers.is(true)); } } @Test - public void shouldFailOnMonthComponent() { - final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString("Duration(\"P6M\")")); + public void shouldFailOnNanosOverflow() { + final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString("Duration(0, 1000000000)")); final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer)); final GremlinParser.DurationLiteralContext ctx = parser.durationLiteral(); try { new GenericLiteralVisitor(new GremlinAntlrToJava()).visitDurationLiteral(ctx); - fail("Month component should have thrown exception"); + fail("Nanos overflow should have thrown exception"); } catch (GremlinParserException gpe) { - assertThat(gpe.getMessage().contains("does not support year or month components"), Matchers.is(true)); + assertThat(gpe.getMessage().contains("nanoseconds must be between 0 and 999999999"), Matchers.is(true)); } } @Test - public void shouldFailOnYearMonthDayComponent() { - final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString("Duration(\"P1Y6M3DT4H\")")); + public void shouldFailOnNegativeSeconds() { + final GremlinLexer lexer = new GremlinLexer(CharStreams.fromString("Duration(-1, 0)")); final GremlinParser parser = new GremlinParser(new CommonTokenStream(lexer)); final GremlinParser.DurationLiteralContext ctx = parser.durationLiteral(); try { new GenericLiteralVisitor(new GremlinAntlrToJava()).visitDurationLiteral(ctx); - fail("Year/month component should have thrown exception"); + fail("Negative seconds should have thrown exception"); } catch (GremlinParserException gpe) { - assertThat(gpe.getMessage().contains("does not support year or month components"), Matchers.is(true)); + assertThat(gpe.getMessage().contains("seconds must be non-negative"), Matchers.is(true)); } } } diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/translator/GremlinTranslatorTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/translator/GremlinTranslatorTest.java index a8792c2b5a..050787e8f8 100644 --- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/translator/GremlinTranslatorTest.java +++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/translator/GremlinTranslatorTest.java @@ -1435,24 +1435,42 @@ public class GremlinTranslatorTest { "g.inject('a')", "Character literals are not supported in JavaScript", "g.inject(SingleChar('a'))"}, - {"g.inject(Duration(\"PT2H30M\"))", + {"g.inject(Duration(9000,0))", null, "g.inject(duration0)", - "g.Inject<object>(XmlConvert.ToTimeSpan(\"PT2H30M\"))", + "g.Inject<object>(TimeSpan.FromTicks(90000000000L))", "g.Inject(time.Duration(9000000000000))", - "g.inject(Duration.parse(\"PT2H30M\"))", - "g.inject(Duration.parse(\"PT2H30M\"))", + "g.inject(Duration.ofSeconds(9000, 0))", + "g.inject(Duration.ofSeconds(9000, 0))", "Duration literals are not supported in JavaScript", "g.inject(timedelta(seconds=9000))"}, - {"g.inject(Duration(\"PT2H-30M\"))", + {"g.inject(Duration(30,0,false))", null, "g.inject(duration0)", - "g.Inject<object>(XmlConvert.ToTimeSpan(\"PT1H30M\"))", - "g.Inject(time.Duration(5400000000000))", - "g.inject(Duration.parse(\"PT2H-30M\"))", - "g.inject(Duration.parse(\"PT2H-30M\"))", + "g.Inject<object>(TimeSpan.FromTicks(300000000L).Negate())", + "g.Inject(time.Duration(-30000000000))", + "g.inject(Duration.ofSeconds(30, 0).negated())", + "g.inject(Duration.ofSeconds(30, 0).negated())", "Duration literals are not supported in JavaScript", - "g.inject(timedelta(seconds=5400))"}, + "g.inject(timedelta(seconds=-30))"}, + {"g.inject(Duration(1,500000000))", + null, + "g.inject(duration0)", + "g.Inject<object>(TimeSpan.FromTicks(15000000L))", + "g.Inject(time.Duration(1500000000))", + "g.inject(Duration.ofSeconds(1, 500000000))", + "g.inject(Duration.ofSeconds(1, 500000000))", + "Duration literals are not supported in JavaScript", + "g.inject(timedelta(seconds=1,microseconds=500000))"}, + {"g.inject(Duration(0,500000000,false))", + null, + "g.inject(duration0)", + "g.Inject<object>(TimeSpan.FromTicks(5000000L).Negate())", + "g.Inject(time.Duration(-500000000))", + "g.inject(Duration.ofSeconds(0, 500000000).negated())", + "g.inject(Duration.ofSeconds(0, 500000000).negated())", + "Duration literals are not supported in JavaScript", + "g.inject(timedelta(seconds=0,microseconds=-500000))"}, {"g.inject(Binary(\"AQID\"))", null, "g.inject(bytebuffer0)", diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTest.java index a86fb444e5..b7f9da3290 100644 --- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTest.java +++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTest.java @@ -38,7 +38,6 @@ import org.junit.runners.Parameterized; import java.math.BigDecimal; import java.math.BigInteger; import java.nio.ByteBuffer; -import java.time.Duration; import java.util.Arrays; import java.util.Collections; import java.util.Date; @@ -134,10 +133,6 @@ public class GremlinLangTest { {g.inject('\''), "g.inject(\"'\"c)"}, // Character - backslash (not in feature file due to Gherkin escaping issues) {g.inject('\\'), "g.inject(\"\\\\\"c)"}, - // Duration - Java-specific: normalization of days to hours, nanosecond precision - {g.inject(Duration.ofDays(1).plusHours(12)), "g.inject(Duration(\"PT36H\"))"}, - {g.inject(Duration.ofNanos(1)), "g.inject(Duration(\"PT0.000000001S\"))"}, - {g.inject(Duration.ofMillis(-500)), "g.inject(Duration(\"PT-0.5S\"))"}, // Binary - byte[] type (distinct from ByteBuffer) {g.inject(new byte[]{1, 2, 3}), "g.inject(Binary(\"AQID\"))"}, {g.inject(new byte[]{}), "g.inject(Binary(\"\"))"}, diff --git a/gremlin-dotnet/build/generate.groovy b/gremlin-dotnet/build/generate.groovy index 234dfb6950..24bc271ad5 100644 --- a/gremlin-dotnet/build/generate.groovy +++ b/gremlin-dotnet/build/generate.groovy @@ -61,7 +61,6 @@ radishGremlinFile.withWriter('UTF-8') { Writer writer -> writer.writeLine('using System;\n' + 'using System.Numerics;\n' + 'using System.Collections.Generic;\n' + - 'using System.Xml;\n' + 'using Gremlin.Net.Structure;\n' + 'using Gremlin.Net.Process.Traversal;\n' + 'using Gremlin.Net.Process.Traversal.Strategy.Optimization;\n' + diff --git a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs index 4de5cdcc12..2dfdc02fd5 100644 --- a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs +++ b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs @@ -28,7 +28,6 @@ using System.Globalization; using System.Numerics; using System.Text; using System.Threading; -using System.Xml; using Gremlin.Net.Process.Traversal.Strategy; using Gremlin.Net.Process.Traversal.Strategy.Decoration; using Gremlin.Net.Structure; @@ -243,8 +242,15 @@ namespace Gremlin.Net.Process.Traversal return $"\"{EscapeJava(charVal.ToString())}\"c"; if (arg is TimeSpan timeSpan) - return $"Duration(\"{XmlConvert.ToString(timeSpan)}\")"; - + { + // Use integer tick arithmetic to avoid float precision loss + var absTicks = Math.Abs(timeSpan.Ticks); + var totalSeconds = absTicks / TimeSpan.TicksPerSecond; + var nanos = (int)((absTicks % TimeSpan.TicksPerSecond) * 100); + return timeSpan < TimeSpan.Zero + ? $"Duration({totalSeconds},{nanos},false)" + : $"Duration({totalSeconds},{nanos})"; + } if (arg is byte[] byteArray) return $"Binary(\"{Convert.ToBase64String(byteArray)}\")"; diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs index 65e7f9fa51..4921813393 100644 --- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs +++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs @@ -549,18 +549,15 @@ namespace Gremlin.Net.IntegrationTest.Gherkin return value[0]; } - private static object ToDuration(string iso8601, string graphName) - { - // Java's Duration.toString() produces negative components like "PT-30S" - // but XmlConvert.ToTimeSpan only supports the leading prefix form "-PT30S". - // Normalize by moving the negative sign to the front. - var normalized = iso8601; - if (!iso8601.StartsWith("-") && iso8601.Contains("-")) - { - // Remove negatives from components and add leading minus - normalized = "-" + iso8601.Replace("-", ""); - } - return System.Xml.XmlConvert.ToTimeSpan(normalized); + private static object ToDuration(string value, string graphName) + { + var parts = value.Split(','); + var seconds = long.Parse(parts[0].Trim()); + var nanos = int.Parse(parts[1].Trim()); + var isPositive = parts.Length < 3 || bool.Parse(parts[2].Trim()); + // TimeSpan has 100-nanosecond tick resolution; sub-100ns values are truncated + var ts = TimeSpan.FromSeconds(seconds) + TimeSpan.FromTicks(nanos / 100); + return isPositive ? ts : ts.Negate(); } private static object ToBinary(string base64, string graphName) diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs index 392d7aa296..bf88cfccfa 100644 --- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs +++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs @@ -32,7 +32,6 @@ using System; using System.Numerics; using System.Collections.Generic; -using System.Xml; using Gremlin.Net.Structure; using Gremlin.Net.Process.Traversal; using Gremlin.Net.Process.Traversal.Strategy.Optimization; @@ -264,17 +263,14 @@ namespace Gremlin.Net.IntegrationTest.Gherkin {"g_V_valuesXdoubleX_isXtypeOfXGType_DOUBLEXX_order_byXascX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "data").Property("double", 3.2d).AddV((string) "data").Property("double", 1.8d).AddV((string) "data").Property("double", 2.5d), (g,p) =>g.V().Values<object>("double").Is(P.TypeOf(GType.Double)).Order().By(Order.Asc)}}, {"g_injectX5_5dX_isXtypeOfXGType_DOUBLEXX_groupCount", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(5.5d).Is(P.TypeOf(GType.Double)).GroupCount<object>()}}, {"g_V_valuesXageX_isXtypeOfXGType_DOUBLEXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Values<object>("age").Is(P.TypeOf(GType.Double))}}, - {"g_injectXDurationXPT2H30MXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(XmlConvert.ToTimeSpan("PT2H30M"))}}, - {"g_injectXDurationXPT0SXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(XmlConvert.ToTimeSpan("PT0S"))}}, - {"g_injectXDurationXPTneg30SXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(XmlConvert.ToTimeSpan("-PT30S"))}}, - {"g_injectXDurationXnegPT30SXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(XmlConvert.ToTimeSpan("-PT30S"))}}, - {"g_injectXDurationXPT0_5SXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(XmlConvert.ToTimeSpan("PT0.5S"))}}, - {"g_injectXDurationXP1DT12HXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(XmlConvert.ToTimeSpan("PT36H"))}}, - {"g_injectXDurationXP2DXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(XmlConvert.ToTimeSpan("PT48H"))}}, - {"g_injectXDurationXPT1H30M15SXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(XmlConvert.ToTimeSpan("PT1H30M15S"))}}, - {"g_injectXDurationXPTneg0_5SXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(XmlConvert.ToTimeSpan("-PT0.5S"))}}, - {"g_valuesXlengthX_isXtypeOfXGType_DURATIONXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "data").Property("length", XmlConvert.ToTimeSpan("PT2H30M")), (g,p) =>g.V().Values<object>("length").Is(P.TypeOf(GType.Duration))}}, - {"g_injectXDurationXPT2H30MXX_isXgtXDurationXPT1HXXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(XmlConvert.ToTimeSpan("PT2H30M")).Is(P.Gt(XmlConvert.ToTimeSpan("PT1H")))}}, + {"g_injectXDurationX9000_0XX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(TimeSpan.FromTicks(90000000000L))}}, + {"g_injectXDurationX0_0XX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(TimeSpan.FromTicks(0L))}}, + {"g_injectXDurationX0_500000000XX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(TimeSpan.FromTicks(5000000L))}}, + {"g_injectXDurationX30_0XX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(TimeSpan.FromTicks(300000000L))}}, + {"g_injectXDurationX30_0_falseXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(TimeSpan.FromTicks(300000000L).Negate())}}, + {"g_injectXDurationX1_500000000_falseXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(TimeSpan.FromTicks(15000000L).Negate())}}, + {"g_valuesXlengthX_isXtypeOfXGType_DURATIONXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "data").Property("length", TimeSpan.FromTicks(90000000000L)), (g,p) =>g.V().Values<object>("length").Is(P.TypeOf(GType.Duration))}}, + {"g_injectXDurationX9000_0XX_isXgtXDurationX3600_0XXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.Inject<object>(TimeSpan.FromTicks(90000000000L)).Is(P.Gt(TimeSpan.FromTicks(36000000000L)))}}, {"g_V_valuesXfloatX_isXtypeOfXGType_FLOATXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "data").Property("float", 2.5), (g,p) =>g.V().Values<object>("float").AsNumber(GType.Float).Is(P.TypeOf(GType.Float))}}, {"g_V_valuesXfloatX_isXtypeOfXGType_FLOATXX_mathXmulX2XX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "data").Property("float", 3.0), (g,p) =>g.V().Values<object>("float").AsNumber(GType.Float).Is(P.TypeOf(GType.Float)).Math("_ * 2")}}, {"g_V_valuesXfloatX_isXtypeOfXGType_FLOATXX_isXeqX1_5XX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.AddV((string) "data").Property("float", 1.5), (g,p) =>g.V().Values<object>("float").AsNumber(GType.Float).Is(P.TypeOf(GType.Float)).Is(P.Eq(1.5))}}, diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/GremlinLangTests.cs b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/GremlinLangTests.cs index e25833ff21..f54f4e359b 100644 --- a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/GremlinLangTests.cs +++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/GremlinLangTests.cs @@ -1057,28 +1057,5 @@ namespace Gremlin.Net.UnitTest.Process.Traversal Assert.Equal("g.inject(\"'\"c)", _g.Inject((object)'\'').GremlinLang.GetGremlin()); } - - // Duration - C#-specific: XmlConvert format with days, tick precision, negative prefix - [Fact] - public void g_Inject_Duration_DaysAndHours() - { - Assert.Equal("g.inject(Duration(\"P1DT12H\"))", - _g.Inject((object)(TimeSpan.FromDays(1) + TimeSpan.FromHours(12))).GremlinLang.GetGremlin()); - } - - [Fact] - public void g_Inject_Duration_TickPrecision() - { - // TimeSpan tick = 100 nanoseconds - Assert.Equal("g.inject(Duration(\"PT0.0000001S\"))", - _g.Inject((object)TimeSpan.FromTicks(1)).GremlinLang.GetGremlin()); - } - - [Fact] - public void g_Inject_Duration_NegativeSubSecond() - { - Assert.Equal("g.inject(Duration(\"-PT0.5S\"))", - _g.Inject((object)TimeSpan.FromMilliseconds(-500)).GremlinLang.GetGremlin()); - } } } diff --git a/gremlin-go/driver/cucumber/cucumberSteps_test.go b/gremlin-go/driver/cucumber/cucumberSteps_test.go index ef4921936a..2d72befcb7 100644 --- a/gremlin-go/driver/cucumber/cucumberSteps_test.go +++ b/gremlin-go/driver/cucumber/cucumberSteps_test.go @@ -47,8 +47,6 @@ type tinkerPopGraph struct { var parsers map[*regexp.Regexp]func(string, string) interface{} -var iso8601DurationRe = regexp.MustCompile(`^(-?)PT(?:(-?\d+)H)?(?:(-?\d+)M)?(?:(-?\d+(?:\.\d+)?)S)?$`) - func init() { parsers = map[*regexp.Regexp]func(string, string) interface{}{ regexp.MustCompile(`^str\[(.*)]$`): func(stringVal, graphName string) interface{} { return stringVal }, //returns the string value as is @@ -136,12 +134,28 @@ func toUuid(stringVal, graphName string) interface{} { return val } -// Parse duration from ISO-8601 string (e.g., "PT2H30M", "PT-30S"). +// Parse duration from seconds,nanos[,isPositive] format. func toDuration(stringVal, graphName string) interface{} { - d, err := parseISO8601Duration(stringVal) + parts := strings.Split(stringVal, ",") + if len(parts) < 2 { + return nil + } + seconds, err := strconv.ParseInt(strings.TrimSpace(parts[0]), 10, 64) + if err != nil { + return nil + } + nanos, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64) if err != nil { return nil } + isPositive := true + if len(parts) >= 3 { + isPositive = strings.TrimSpace(parts[2]) == "true" + } + d := time.Duration(seconds)*time.Second + time.Duration(nanos) + if !isPositive { + d = -d + } return d } @@ -154,33 +168,6 @@ func toBinary(stringVal, graphName string) interface{} { return &gremlingo.ByteBuffer{Data: data} } -// parseISO8601Duration parses an ISO-8601 duration string (as produced by Java's Duration.toString()) -// into a time.Duration. Supports formats like "PT2H30M", "PT-30S", "PT0.5S". -func parseISO8601Duration(s string) (time.Duration, error) { - m := iso8601DurationRe.FindStringSubmatch(s) - if m == nil { - return 0, fmt.Errorf("invalid ISO-8601 duration: %s", s) - } - sign := time.Duration(1) - if m[1] == "-" { - sign = -1 - } - var d time.Duration - if m[2] != "" { - h, _ := strconv.ParseInt(m[2], 10, 64) - d += time.Duration(h) * time.Hour - } - if m[3] != "" { - min, _ := strconv.ParseInt(m[3], 10, 64) - d += time.Duration(min) * time.Minute - } - if m[4] != "" { - sec, _ := strconv.ParseFloat(m[4], 64) - d += time.Duration(sec * float64(time.Second)) - } - return sign * d, nil -} - // Parse numeric. func toNumeric(stringVal, graphName string) interface{} { if strings.Contains(stringVal, ".") { diff --git a/gremlin-go/driver/cucumber/gremlin.go b/gremlin-go/driver/cucumber/gremlin.go index b20c1ac9fd..2c821d785d 100644 --- a/gremlin-go/driver/cucumber/gremlin.go +++ b/gremlin-go/driver/cucumber/gremlin.go @@ -233,17 +233,14 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[ "g_V_valuesXdoubleX_isXtypeOfXGType_DOUBLEXX_order_byXascX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("data").Property("double", 3.2).AddV("data").Property("double", 1.8).AddV("data").Property("double", 2.5)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("double").Is(gremlingo.P.TypeOf(gremlingo.GType.Double)).Order().By(gremlingo.Order.Asc)}}, "g_injectX5_5dX_isXtypeOfXGType_DOUBLEXX_groupCount": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(5.5).Is(gremlingo.P.TypeOf(gremlingo.GType.Double)).GroupCount()}}, "g_V_valuesXageX_isXtypeOfXGType_DOUBLEXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("age").Is(gremlingo.P.TypeOf(gremlingo.GType.Double))}}, - "g_injectXDurationXPT2H30MXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(time.Duration(9000000000000))}}, - "g_injectXDurationXPT0SXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(time.Duration(0))}}, - "g_injectXDurationXPTneg30SXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(time.Duration(-30000000000))}}, - "g_injectXDurationXnegPT30SXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(time.Duration(-30000000000))}}, - "g_injectXDurationXPT0_5SXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(time.Duration(500000000))}}, - "g_injectXDurationXP1DT12HXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(time.Duration(129600000000000))}}, - "g_injectXDurationXP2DXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(time.Duration(172800000000000))}}, - "g_injectXDurationXPT1H30M15SXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(time.Duration(5415000000000))}}, - "g_injectXDurationXPTneg0_5SXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(time.Duration(-500000000))}}, + "g_injectXDurationX9000_0XX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(time.Duration(9000000000000))}}, + "g_injectXDurationX0_0XX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(time.Duration(0))}}, + "g_injectXDurationX0_500000000XX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(time.Duration(500000000))}}, + "g_injectXDurationX30_0XX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(time.Duration(30000000000))}}, + "g_injectXDurationX30_0_falseXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(time.Duration(-30000000000))}}, + "g_injectXDurationX1_500000000_falseXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(time.Duration(-1500000000))}}, "g_valuesXlengthX_isXtypeOfXGType_DURATIONXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("data").Property("length", time.Duration(9000000000000))}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("length").Is(gremlingo.P.TypeOf(gremlingo.GType.Duration))}}, - "g_injectXDurationXPT2H30MXX_isXgtXDurationXPT1HXXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(time.Duration(9000000000000)).Is(gremlingo.P.Gt(time.Duration(3600000000000)))}}, + "g_injectXDurationX9000_0XX_isXgtXDurationX3600_0XXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(time.Duration(9000000000000)).Is(gremlingo.P.Gt(time.Duration(3600000000000)))}}, "g_V_valuesXfloatX_isXtypeOfXGType_FLOATXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("data").Property("float", 2.5)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("float").AsNumber(gremlingo.GType.Float).Is(gremlingo.P.TypeOf(gremlingo.GType.Float))}}, "g_V_valuesXfloatX_isXtypeOfXGType_FLOATXX_mathXmulX2XX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("data").Property("float", 3.0)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("float").AsNumber(gremlingo.GType.Float).Is(gremlingo.P.TypeOf(gremlingo.GType.Float)).Math("_ * 2")}}, "g_V_valuesXfloatX_isXtypeOfXGType_FLOATXX_isXeqX1_5XX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.AddV("data").Property("float", 1.5)}, func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Values("float").AsNumber(gremlingo.GType.Float).Is(gremlingo.P.TypeOf(gremlingo.GType.Float)).Is(gremlingo.P.Eq(1.5))}}, diff --git a/gremlin-go/driver/gremlinlang.go b/gremlin-go/driver/gremlinlang.go index b063765f47..2362515e5f 100644 --- a/gremlin-go/driver/gremlinlang.go +++ b/gremlin-go/driver/gremlinlang.go @@ -137,54 +137,6 @@ func escapeString(s string) string { return sb.String() } -// formatISO8601Duration formats a time.Duration as an ISO-8601 duration string -// matching Java's Duration.toString() output (e.g., "PT2H30M", "PT-30S", "PT0.5S"). -func formatISO8601Duration(d time.Duration) string { - if d == 0 { - return "PT0S" - } - - totalSeconds := int64(d / time.Second) - nanos := int64(d % time.Second) - - hours := totalSeconds / 3600 - minutes := (totalSeconds % 3600) / 60 - seconds := totalSeconds % 60 - - var sb strings.Builder - sb.WriteString("PT") - - if hours != 0 { - fmt.Fprintf(&sb, "%dH", hours) - } - if minutes != 0 { - fmt.Fprintf(&sb, "%dM", minutes) - } - if seconds != 0 || nanos != 0 { - if nanos == 0 { - fmt.Fprintf(&sb, "%dS", seconds) - } else { - absNanos := nanos - if absNanos < 0 { - absNanos = -absNanos - } - frac := fmt.Sprintf(".%09d", absNanos) - frac = strings.TrimRight(frac, "0") - // for negative sub-second durations where seconds truncated to 0 - if seconds == 0 && d < 0 { - fmt.Fprintf(&sb, "-0%sS", frac) - } else { - fmt.Fprintf(&sb, "%d%sS", seconds, frac) - } - } - } - - if hours == 0 && minutes == 0 && seconds == 0 && nanos == 0 { - sb.WriteString("0S") - } - - return sb.String() -} func (gl *GremlinLang) argAsString(arg interface{}) (string, error) { if arg == nil { @@ -320,7 +272,17 @@ func (gl *GremlinLang) argAsString(arg interface{}) (string, error) { case uuid.UUID: return fmt.Sprintf("UUID(\"%v\")", v.String()), nil case time.Duration: - return fmt.Sprintf("Duration(\"%s\")", formatISO8601Duration(v)), nil + isNegative := v < 0 + abs := v + if isNegative { + abs = -v + } + totalSeconds := int64(abs / time.Second) + nanos := int64(abs % time.Second) + if isNegative { + return fmt.Sprintf("Duration(%d,%d,false)", totalSeconds, nanos), nil + } + return fmt.Sprintf("Duration(%d,%d)", totalSeconds, nanos), nil case ByteBuffer: return fmt.Sprintf("Binary(\"%s\")", base64.StdEncoding.EncodeToString(v.Data)), nil case *ByteBuffer: diff --git a/gremlin-go/driver/gremlinlang_test.go b/gremlin-go/driver/gremlinlang_test.go index 6e5f5d98cf..9d3ee5b93c 100644 --- a/gremlin-go/driver/gremlinlang_test.go +++ b/gremlin-go/driver/gremlinlang_test.go @@ -680,28 +680,6 @@ func Test_GremlinLang(t *testing.T) { }, equals: "g.V().has(\"name\",\"\\\"marko\\n\\r\\t\\b\\f\\\"\")", }, - // Duration - Go-specific: nanosecond precision, negative sub-second, days+hours normalization - { - name: "g_Inject_Duration_NanosecondPrecision", - assert: func(g *GraphTraversalSource) *GraphTraversal { - return g.Inject(time.Nanosecond) - }, - equals: `g.inject(Duration("PT0.000000001S"))`, - }, - { - name: "g_Inject_Duration_NegativeSubSecond", - assert: func(g *GraphTraversalSource) *GraphTraversal { - return g.Inject(-500 * time.Millisecond) - }, - equals: `g.inject(Duration("PT-0.5S"))`, - }, - { - name: "g_Inject_Duration_DaysAndHours", - assert: func(g *GraphTraversalSource) *GraphTraversal { - return g.Inject(36 * time.Hour) - }, - equals: `g.inject(Duration("PT36H"))`, - }, // Binary - []byte type (distinct from ByteBuffer) { name: "g_Inject_ByteSlice", diff --git a/gremlin-js/gremlin-javascript/lib/language/translator/DotNetTranslateVisitor.ts b/gremlin-js/gremlin-javascript/lib/language/translator/DotNetTranslateVisitor.ts index a497573696..8cb11b125b 100644 --- a/gremlin-js/gremlin-javascript/lib/language/translator/DotNetTranslateVisitor.ts +++ b/gremlin-js/gremlin-javascript/lib/language/translator/DotNetTranslateVisitor.ts @@ -235,33 +235,16 @@ export default class DotNetTranslateVisitor extends TranslateVisitor { } visitDurationLiteral(ctx: any): void { - const iso8601 = TranslateVisitor.removeFirstAndLastCharacters(ctx.stringLiteral().getText()); - // .NET's XmlConvert.ToTimeSpan requires "-PT30S" not "PT-30S", so parse and - // re-emit in the normalized form that .NET expects - const { totalSeconds, nanos } = parseDurationComponents(iso8601); - // combine into total nanos for correct decomposition - const totalNanos = totalSeconds * 1_000_000_000 + nanos; - const isNegative = totalNanos < 0; - const absNanos = Math.abs(totalNanos); - const absSec = Math.floor(absNanos / 1_000_000_000); - const fracNanos = absNanos % 1_000_000_000; - const hours = Math.floor(absSec / 3600); - const minutes = Math.floor((absSec % 3600) / 60); - const seconds = absSec % 60; - let normalized = 'PT'; - if (hours !== 0) normalized += `${hours}H`; - if (minutes !== 0) normalized += `${minutes}M`; - if (seconds !== 0 || fracNanos !== 0) { - if (fracNanos === 0) { - normalized += `${seconds}S`; - } else { - const frac = fracNanos.toString().padStart(9, '0').replace(/0+$/, ''); - normalized += `${seconds}.${frac}S`; - } + const seconds = parseInt(ctx.integerLiteral(0).getText(), 10); + const nanos = parseInt(ctx.integerLiteral(1).getText(), 10); + const isPositive = ctx.booleanLiteral() === null || + ctx.booleanLiteral().getText() === 'true'; + // Convert to ticks: 1 tick = 100 nanoseconds, 1 second = 10,000,000 ticks + const ticks = seconds * 10_000_000 + Math.floor(nanos / 100); + this.sb.push(`TimeSpan.FromTicks(${ticks}L)`); + if (!isPositive) { + this.sb.push('.Negate()'); } - if (normalized === 'PT') normalized = 'PT0S'; - if (isNegative) normalized = '-' + normalized; - this.sb.push(`XmlConvert.ToTimeSpan("${normalized}")`); } visitBinaryLiteral(ctx: any): void { @@ -890,48 +873,6 @@ function capitalize(s: string): string { return s.charAt(0).toUpperCase() + s.slice(1); } -/** - * Parses an ISO-8601 duration string into total seconds and nanoseconds, - * normalizing so nanos is always 0..999999999 (matching Java's Duration). - */ -function parseDurationComponents(iso8601: string): { totalSeconds: number; nanos: number } { - const match = iso8601.match(/^([+-]?)P(?:(\d+)D)?T?(?:([+-]?\d+)H)?(?:([+-]?\d+)M)?(?:([+-]?\d+(?:\.\d+)?)S)?$/); - if (!match) { - throw new TranslatorException(`Invalid ISO-8601 duration: ${iso8601}`); - } - const negative = match[1] === '-'; - const days = match[2] ? parseInt(match[2], 10) : 0; - const hours = match[3] ? parseInt(match[3], 10) : 0; - const minutes = match[4] ? parseInt(match[4], 10) : 0; - - let totalSeconds = days * 86400 + hours * 3600 + minutes * 60; - let nanos = 0; - - if (match[5]) { - const secParts = match[5].split('.'); - totalSeconds += parseInt(secParts[0], 10); - if (secParts.length > 1) { - nanos = parseInt(secParts[1].padEnd(9, '0').substring(0, 9), 10); - if (parseInt(secParts[0], 10) < 0 || (parseInt(secParts[0], 10) === 0 && match[5].startsWith('-'))) { - nanos = -nanos; - } - } - } - - if (negative) { - totalSeconds = -totalSeconds; - nanos = -nanos; - } - - // Normalize so nanos is always 0..999999999 (matching Java's Duration) - if (nanos < 0) { - totalSeconds -= 1; - nanos += 1_000_000_000; - } - - return { totalSeconds, nanos }; -} - /** * Formats a datetime string the same way Java's OffsetDateTime.toString() does: * - Truncates seconds if both seconds and milliseconds are 0 diff --git a/gremlin-js/gremlin-javascript/lib/language/translator/GoTranslateVisitor.ts b/gremlin-js/gremlin-javascript/lib/language/translator/GoTranslateVisitor.ts index 0b168d00d8..c95cc6de32 100644 --- a/gremlin-js/gremlin-javascript/lib/language/translator/GoTranslateVisitor.ts +++ b/gremlin-js/gremlin-javascript/lib/language/translator/GoTranslateVisitor.ts @@ -262,9 +262,13 @@ export default class GoTranslateVisitor extends TranslateVisitor { } visitDurationLiteral(ctx: any): void { - const iso8601 = TranslateVisitor.removeFirstAndLastCharacters(ctx.stringLiteral().getText()); - const nanos = parseISO8601DurationToNanos(iso8601); - this.sb.push(`time.Duration(${nanos})`); + const seconds = parseInt(ctx.integerLiteral(0).getText(), 10); + const nanos = parseInt(ctx.integerLiteral(1).getText(), 10); + const isPositive = ctx.booleanLiteral() === null || + ctx.booleanLiteral().getText() === 'true'; + // Use BigInt to avoid precision loss for durations over ~104 days + const totalNanos = BigInt(seconds) * 1_000_000_000n + BigInt(nanos); + this.sb.push(`time.Duration(${isPositive ? totalNanos : -totalNanos})`); } visitBinaryLiteral(ctx: any): void { @@ -371,38 +375,6 @@ export default class GoTranslateVisitor extends TranslateVisitor { } } -/** - * Parses an ISO-8601 duration string into total nanoseconds, - * matching the Java GoTranslateVisitor behavior. - */ -function parseISO8601DurationToNanos(iso8601: string): number { - const match = iso8601.match(/^([+-]?)P(?:(\d+)D)?T?(?:([+-]?\d+)H)?(?:([+-]?\d+)M)?(?:([+-]?\d+(?:\.\d+)?)S)?$/); - if (!match) { - throw new TranslatorException(`Invalid ISO-8601 duration: ${iso8601}`); - } - const negative = match[1] === '-'; - const days = match[2] ? parseInt(match[2], 10) : 0; - const hours = match[3] ? parseInt(match[3], 10) : 0; - const minutes = match[4] ? parseInt(match[4], 10) : 0; - - let totalNanos = (days * 86400 + hours * 3600 + minutes * 60) * 1_000_000_000; - - if (match[5]) { - const secParts = match[5].split('.'); - totalNanos += parseInt(secParts[0], 10) * 1_000_000_000; - if (secParts.length > 1) { - const fracStr = secParts[1].padEnd(9, '0').substring(0, 9); - let fracNanos = parseInt(fracStr, 10); - if (parseInt(secParts[0], 10) < 0 || (parseInt(secParts[0], 10) === 0 && match[5].startsWith('-'))) { - fracNanos = -fracNanos; - } - totalNanos += fracNanos; - } - } - - return negative ? -totalNanos : totalNanos; -} - /** * Decodes a base64 string into an array of unsigned byte values. */ diff --git a/gremlin-js/gremlin-javascript/lib/language/translator/GroovyTranslateVisitor.ts b/gremlin-js/gremlin-javascript/lib/language/translator/GroovyTranslateVisitor.ts index d77946da39..f061975e2b 100644 --- a/gremlin-js/gremlin-javascript/lib/language/translator/GroovyTranslateVisitor.ts +++ b/gremlin-js/gremlin-javascript/lib/language/translator/GroovyTranslateVisitor.ts @@ -124,9 +124,14 @@ export default class GroovyTranslateVisitor extends TranslateVisitor { } visitDurationLiteral(ctx: any): void { - this.sb.push('Duration.parse('); - this.sb.push(ctx.stringLiteral().getText()); - this.sb.push(')'); + const seconds = parseInt(ctx.integerLiteral(0).getText(), 10); + const nanos = parseInt(ctx.integerLiteral(1).getText(), 10); + const isPositive = ctx.booleanLiteral() === null || + ctx.booleanLiteral().getText() === 'true'; + this.sb.push(`Duration.ofSeconds(${seconds}, ${nanos})`); + if (!isPositive) { + this.sb.push('.negated()'); + } } visitBinaryLiteral(ctx: any): void { diff --git a/gremlin-js/gremlin-javascript/lib/language/translator/JavaTranslateVisitor.ts b/gremlin-js/gremlin-javascript/lib/language/translator/JavaTranslateVisitor.ts index 414b76b6aa..fbbe38d1b1 100644 --- a/gremlin-js/gremlin-javascript/lib/language/translator/JavaTranslateVisitor.ts +++ b/gremlin-js/gremlin-javascript/lib/language/translator/JavaTranslateVisitor.ts @@ -210,9 +210,14 @@ export default class JavaTranslateVisitor extends TranslateVisitor { } visitDurationLiteral(ctx: any): void { - this.sb.push('Duration.parse('); - this.sb.push(ctx.stringLiteral().getText()); - this.sb.push(')'); + const seconds = parseInt(ctx.integerLiteral(0).getText(), 10); + const nanos = parseInt(ctx.integerLiteral(1).getText(), 10); + const isPositive = ctx.booleanLiteral() === null || + ctx.booleanLiteral().getText() === 'true'; + this.sb.push(`Duration.ofSeconds(${seconds}, ${nanos})`); + if (!isPositive) { + this.sb.push('.negated()'); + } } visitBinaryLiteral(ctx: any): void { diff --git a/gremlin-js/gremlin-javascript/lib/language/translator/PythonTranslateVisitor.ts b/gremlin-js/gremlin-javascript/lib/language/translator/PythonTranslateVisitor.ts index 9be9365804..e9e4de9967 100644 --- a/gremlin-js/gremlin-javascript/lib/language/translator/PythonTranslateVisitor.ts +++ b/gremlin-js/gremlin-javascript/lib/language/translator/PythonTranslateVisitor.ts @@ -224,13 +224,18 @@ export default class PythonTranslateVisitor extends TranslateVisitor { } visitDurationLiteral(ctx: any): void { - const iso8601 = TranslateVisitor.removeFirstAndLastCharacters(ctx.stringLiteral().getText()); - const { totalSeconds, nanos } = parseISO8601Duration(iso8601); - if (nanos === 0) { + const seconds = parseInt(ctx.integerLiteral(0).getText(), 10); + const nanos = parseInt(ctx.integerLiteral(1).getText(), 10); + const isPositive = ctx.booleanLiteral() === null || + ctx.booleanLiteral().getText() === 'true'; + const totalSeconds = isPositive ? seconds : -seconds; + // Python's timedelta has microsecond resolution; sub-microsecond nanos are truncated + const micros = Math.floor(nanos / 1000); + if (micros === 0) { this.sb.push(`timedelta(seconds=${totalSeconds})`); } else { - const micros = Math.floor(nanos / 1000); - this.sb.push(`timedelta(seconds=${totalSeconds},microseconds=${micros})`); + const totalMicros = isPositive ? micros : -micros; + this.sb.push(`timedelta(seconds=${totalSeconds},microseconds=${totalMicros})`); } } @@ -346,45 +351,4 @@ function convertCamelCaseToSnakeCase(camelCase: string): string { return result; } -/** - * Parses an ISO-8601 duration string (as produced by Java's Duration.toString()) - * into total seconds and nanoseconds, matching the Java PythonTranslateVisitor behavior. - */ -function parseISO8601Duration(iso8601: string): { totalSeconds: number; nanos: number } { - // Use a regex that matches Java's Duration.toString() output - const match = iso8601.match(/^([+-]?)P(?:(\d+)D)?T?(?:([+-]?\d+)H)?(?:([+-]?\d+)M)?(?:([+-]?\d+(?:\.\d+)?)S)?$/); - if (!match) { - throw new TranslatorException(`Invalid ISO-8601 duration: ${iso8601}`); - } - const negative = match[1] === '-'; - const days = match[2] ? parseInt(match[2], 10) : 0; - const hours = match[3] ? parseInt(match[3], 10) : 0; - const minutes = match[4] ? parseInt(match[4], 10) : 0; - - let totalSeconds = days * 86400 + hours * 3600 + minutes * 60; - let nanos = 0; - - if (match[5]) { - const secParts = match[5].split('.'); - totalSeconds += parseInt(secParts[0], 10); - if (secParts.length > 1) { - nanos = parseInt(secParts[1].padEnd(9, '0').substring(0, 9), 10); - if (parseInt(secParts[0], 10) < 0 || (parseInt(secParts[0], 10) === 0 && match[5].startsWith('-'))) { - nanos = -nanos; - } - } - } - if (negative) { - totalSeconds = -totalSeconds; - nanos = -nanos; - } - - // Normalize so nanos is always 0..999999999 (matching Java's Duration representation) - if (nanos < 0) { - totalSeconds -= 1; - nanos += 1_000_000_000; - } - - return { totalSeconds, nanos }; -} diff --git a/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js b/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js index 5338791541..2b0adf72b4 100644 --- a/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js +++ b/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js @@ -265,17 +265,14 @@ const gremlins = { g_V_valuesXdoubleX_isXtypeOfXGType_DOUBLEXX_order_byXascX: [function({g}) { return g.addV("data").property("double", 3.2).addV("data").property("double", 1.8).addV("data").property("double", 2.5) }, function({g}) { return g.V().values("double").is(P.typeOf(GType.double)).order().by(Order.asc) }], g_injectX5_5dX_isXtypeOfXGType_DOUBLEXX_groupCount: [function({g}) { return g.inject(5.5).is(P.typeOf(GType.double)).groupCount() }], g_V_valuesXageX_isXtypeOfXGType_DOUBLEXX: [function({g}) { return g.V().values("age").is(P.typeOf(GType.double)) }], - g_injectXDurationXPT2H30MXX: [function({g}) { return null }], - g_injectXDurationXPT0SXX: [function({g}) { return null }], - g_injectXDurationXPTneg30SXX: [function({g}) { return null }], - g_injectXDurationXnegPT30SXX: [function({g}) { return null }], - g_injectXDurationXPT0_5SXX: [function({g}) { return null }], - g_injectXDurationXP1DT12HXX: [function({g}) { return null }], - g_injectXDurationXP2DXX: [function({g}) { return null }], - g_injectXDurationXPT1H30M15SXX: [function({g}) { return null }], - g_injectXDurationXPTneg0_5SXX: [function({g}) { return null }], + g_injectXDurationX9000_0XX: [function({g}) { return null }], + g_injectXDurationX0_0XX: [function({g}) { return null }], + g_injectXDurationX0_500000000XX: [function({g}) { return null }], + g_injectXDurationX30_0XX: [function({g}) { return null }], + g_injectXDurationX30_0_falseXX: [function({g}) { return null }], + g_injectXDurationX1_500000000_falseXX: [function({g}) { return null }], g_valuesXlengthX_isXtypeOfXGType_DURATIONXX: [function({g}) { return null }], - g_injectXDurationXPT2H30MXX_isXgtXDurationXPT1HXXX: [function({g}) { return null }], + g_injectXDurationX9000_0XX_isXgtXDurationX3600_0XXX: [function({g}) { return null }], g_V_valuesXfloatX_isXtypeOfXGType_FLOATXX: [function({g}) { return g.addV("data").property("float", 2.5) }, function({g}) { return g.V().values("float").asNumber(GType.float).is(P.typeOf(GType.float)) }], g_V_valuesXfloatX_isXtypeOfXGType_FLOATXX_mathXmulX2XX: [function({g}) { return g.addV("data").property("float", 3.0) }, function({g}) { return g.V().values("float").asNumber(GType.float).is(P.typeOf(GType.float)).math("_ * 2") }], g_V_valuesXfloatX_isXtypeOfXGType_FLOATXX_isXeqX1_5XX: [function({g}) { return g.addV("data").property("float", 1.5) }, function({g}) { return g.V().values("float").asNumber(GType.float).is(P.typeOf(GType.float)).is(P.eq(1.5)) }], diff --git a/gremlin-language/src/main/antlr4/Gremlin.g4 b/gremlin-language/src/main/antlr4/Gremlin.g4 index 68b1fc58c8..6798ebde55 100644 --- a/gremlin-language/src/main/antlr4/Gremlin.g4 +++ b/gremlin-language/src/main/antlr4/Gremlin.g4 @@ -1721,7 +1721,7 @@ characterLiteral ; durationLiteral - : K_DURATIONC LPAREN stringLiteral RPAREN + : K_DURATIONC LPAREN integerLiteral COMMA integerLiteral (COMMA booleanLiteral)? RPAREN ; binaryLiteral diff --git a/gremlin-python/src/main/python/gremlin_python/process/traversal.py b/gremlin-python/src/main/python/gremlin_python/process/traversal.py index 41ec218a36..4a531d5845 100644 --- a/gremlin-python/src/main/python/gremlin_python/process/traversal.py +++ b/gremlin-python/src/main/python/gremlin_python/process/traversal.py @@ -805,44 +805,6 @@ GREMLIN LANGUAGE ''' -def _format_iso8601_duration(td): - """Formats a timedelta as an ISO-8601 duration string matching Java's Duration.toString().""" - if td == timedelta(0): - return "PT0S" - - total_seconds = int(td.total_seconds()) - microseconds = td.microseconds if td.total_seconds() >= 0 else -td.microseconds - - # detect negative sub-second durations where int() truncates to 0 - is_negative = td.total_seconds() < 0 - - # use int() for truncation toward zero (not // which is floor division in Python) - hours = int(total_seconds / 3600) - minutes = int((total_seconds - hours * 3600) / 60) - seconds = total_seconds - hours * 3600 - minutes * 60 - - parts = ["PT"] - if hours != 0: - parts.append(f"{hours}H") - if minutes != 0: - parts.append(f"{minutes}M") - if seconds != 0 or microseconds != 0: - if microseconds == 0: - parts.append(f"{seconds}S") - else: - frac = abs(microseconds) / 1_000_000 - frac_str = f"{frac:.6f}".lstrip("0").rstrip("0") - # for negative sub-second durations where seconds truncated to 0 - if seconds == 0 and is_negative: - parts.append(f"-0{frac_str}S") - else: - parts.append(f"{seconds}{frac_str}S") - if len(parts) == 1: - parts.append("0S") - - return "".join(parts) - - class GremlinLang(object): conn_p = ['and', 'or'] @@ -922,7 +884,15 @@ class GremlinLang(object): return f'UUID("{arg}")' if isinstance(arg, timedelta): - return f'Duration("{_format_iso8601_duration(arg)}")' + is_negative = arg.total_seconds() < 0 + abs_td = -arg if is_negative else arg + # Use integer components directly to avoid float precision loss + seconds = abs_td.days * 86400 + abs_td.seconds + nanos = abs_td.microseconds * 1000 + if is_negative: + return f'Duration({seconds},{nanos},false)' + else: + return f'Duration({seconds},{nanos})' if isinstance(arg, bytes): return f'Binary("{base64.b64encode(arg).decode("ascii")}")' diff --git a/gremlin-python/src/main/python/tests/feature/feature_steps.py b/gremlin-python/src/main/python/tests/feature/feature_steps.py index 7ed183413c..c5365d009f 100644 --- a/gremlin-python/src/main/python/tests/feature/feature_steps.py +++ b/gremlin-python/src/main/python/tests/feature/feature_steps.py @@ -346,16 +346,15 @@ def _split_by_element(s): return results -def _parse_iso8601_duration(iso_str): - """Parses an ISO-8601 duration string (as produced by Java's Duration.toString()) into a timedelta.""" - m = re.match(r'^(-?)PT(?:(-?\d+)H)?(?:(-?\d+)M)?(?:(-?\d+(?:\.\d+)?)S)?$', iso_str) - if not m: - raise ValueError("Invalid ISO-8601 duration: " + iso_str) - sign = -1 if m.group(1) == '-' else 1 - hours = int(m.group(2)) if m.group(2) else 0 - minutes = int(m.group(3)) if m.group(3) else 0 - seconds = float(m.group(4)) if m.group(4) else 0.0 - return timedelta(hours=sign * hours, minutes=sign * minutes, seconds=sign * seconds) +def _parse_duration(dur_str): + """Parses a duration from seconds,nanos[,isPositive] format into a timedelta.""" + parts = dur_str.split(",") + seconds = int(parts[0].strip()) + nanos = int(parts[1].strip()) + is_positive = len(parts) < 3 or parts[2].strip() == "true" + # timedelta has microsecond resolution; sub-microsecond nanos are truncated + td = timedelta(seconds=seconds, microseconds=nanos // 1000) + return td if is_positive else -td def _convert(val, ctx): @@ -382,7 +381,7 @@ def _convert(val, ctx): elif isinstance(val, str) and re.match(r"^char\[.\]$", val): # parse char return SingleChar(val[5:-1]) elif isinstance(val, str) and re.match(r"^dur\[.*\]$", val): # parse duration - return _parse_iso8601_duration(val[4:-1]) + return _parse_duration(val[4:-1]) elif isinstance(val, str) and re.match(r"^bin\[.*\]$", val): # parse binary return base64.b64decode(val[4:-1]) elif isinstance(val, str) and re.match(r"^d\[NaN\]$", val): # parse nan diff --git a/gremlin-python/src/main/python/tests/feature/gremlin.py b/gremlin-python/src/main/python/tests/feature/gremlin.py index e2854391a4..9cd84e34ff 100644 --- a/gremlin-python/src/main/python/tests/feature/gremlin.py +++ b/gremlin-python/src/main/python/tests/feature/gremlin.py @@ -238,17 +238,14 @@ world.gremlins = { 'g_V_valuesXdoubleX_isXtypeOfXGType_DOUBLEXX_order_byXascX': [(lambda g:g.add_v('data').property('double', 3.2).add_v('data').property('double', 1.8).add_v('data').property('double', 2.5)), (lambda g:g.V().values('double').is_(P.type_of(GType.DOUBLE)).order().by(Order.asc))], 'g_injectX5_5dX_isXtypeOfXGType_DOUBLEXX_groupCount': [(lambda g:g.inject(5.5).is_(P.type_of(GType.DOUBLE)).group_count())], 'g_V_valuesXageX_isXtypeOfXGType_DOUBLEXX': [(lambda g:g.V().values('age').is_(P.type_of(GType.DOUBLE)))], - 'g_injectXDurationXPT2H30MXX': [(lambda g:g.inject(timedelta(seconds=9000)))], - 'g_injectXDurationXPT0SXX': [(lambda g:g.inject(timedelta(seconds=0)))], - 'g_injectXDurationXPTneg30SXX': [(lambda g:g.inject(timedelta(seconds=-30)))], - 'g_injectXDurationXnegPT30SXX': [(lambda g:g.inject(timedelta(seconds=-30)))], - 'g_injectXDurationXPT0_5SXX': [(lambda g:g.inject(timedelta(seconds=0,microseconds=500000)))], - 'g_injectXDurationXP1DT12HXX': [(lambda g:g.inject(timedelta(seconds=129600)))], - 'g_injectXDurationXP2DXX': [(lambda g:g.inject(timedelta(seconds=172800)))], - 'g_injectXDurationXPT1H30M15SXX': [(lambda g:g.inject(timedelta(seconds=5415)))], - 'g_injectXDurationXPTneg0_5SXX': [(lambda g:g.inject(timedelta(seconds=-1,microseconds=500000)))], + 'g_injectXDurationX9000_0XX': [(lambda g:g.inject(timedelta(seconds=9000)))], + 'g_injectXDurationX0_0XX': [(lambda g:g.inject(timedelta(seconds=0)))], + 'g_injectXDurationX0_500000000XX': [(lambda g:g.inject(timedelta(seconds=0,microseconds=500000)))], + 'g_injectXDurationX30_0XX': [(lambda g:g.inject(timedelta(seconds=30)))], + 'g_injectXDurationX30_0_falseXX': [(lambda g:g.inject(timedelta(seconds=-30)))], + 'g_injectXDurationX1_500000000_falseXX': [(lambda g:g.inject(timedelta(seconds=-1,microseconds=-500000)))], 'g_valuesXlengthX_isXtypeOfXGType_DURATIONXX': [(lambda g:g.add_v('data').property('length', timedelta(seconds=9000))), (lambda g:g.V().values('length').is_(P.type_of(GType.DURATION)))], - 'g_injectXDurationXPT2H30MXX_isXgtXDurationXPT1HXXX': [(lambda g:g.inject(timedelta(seconds=9000)).is_(P.gt(timedelta(seconds=3600))))], + 'g_injectXDurationX9000_0XX_isXgtXDurationX3600_0XXX': [(lambda g:g.inject(timedelta(seconds=9000)).is_(P.gt(timedelta(seconds=3600))))], 'g_V_valuesXfloatX_isXtypeOfXGType_FLOATXX': [(lambda g:g.add_v('data').property('float', 2.5)), (lambda g:g.V().values('float').as_number(GType.FLOAT).is_(P.type_of(GType.FLOAT)))], 'g_V_valuesXfloatX_isXtypeOfXGType_FLOATXX_mathXmulX2XX': [(lambda g:g.add_v('data').property('float', 3.0)), (lambda g:g.V().values('float').as_number(GType.FLOAT).is_(P.type_of(GType.FLOAT)).math('_ * 2'))], 'g_V_valuesXfloatX_isXtypeOfXGType_FLOATXX_isXeqX1_5XX': [(lambda g:g.add_v('data').property('float', 1.5)), (lambda g:g.V().values('float').as_number(GType.FLOAT).is_(P.type_of(GType.FLOAT)).is_(P.eq(1.5)))], diff --git a/gremlin-python/src/main/python/tests/unit/process/test_gremlin_lang.py b/gremlin-python/src/main/python/tests/unit/process/test_gremlin_lang.py index 7abfc2fa31..0f1351e8c1 100644 --- a/gremlin-python/src/main/python/tests/unit/process/test_gremlin_lang.py +++ b/gremlin-python/src/main/python/tests/unit/process/test_gremlin_lang.py @@ -492,14 +492,6 @@ class TestGremlinLang(object): tests.append([g.inject(SingleChar("'")), "g.inject(\"'\"c)"]) - # Duration - Python-specific: microsecond precision, days+hours normalization - # 124 - tests.append([g.inject(timedelta(microseconds=1)), - 'g.inject(Duration("PT0.000001S"))']) - # 125 - tests.append([g.inject(timedelta(days=1, hours=12)), - 'g.inject(Duration("PT36H"))']) - for t in range(len(tests)): gremlin_lang = tests[t][0].gremlin_lang.get_gremlin() assert gremlin_lang == tests[t][1] diff --git a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java index 208db90d46..45f29c17b6 100644 --- a/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java +++ b/gremlin-test/src/main/java/org/apache/tinkerpop/gremlin/features/StepDefinition.java @@ -169,7 +169,13 @@ public final class StepDefinition { add(Pair.with(Pattern.compile("dt\\[(.*)\\]"), s -> String.format("datetime('%s')", s))); add(Pair.with(Pattern.compile("char\\[(.)\\]"), s -> String.format("\"%s\"c", s))); - add(Pair.with(Pattern.compile("dur\\[(.*)\\]"), s -> String.format("Duration(\"%s\")", s))); + add(Pair.with(Pattern.compile("dur\\[(.*)\\]"), s -> { + final String[] parts = s.split(","); + if (parts.length == 2) + return String.format("Duration(%s, %s)", parts[0].trim(), parts[1].trim()); + else + return String.format("Duration(%s, %s, %s)", parts[0].trim(), parts[1].trim(), parts[2].trim()); + })); add(Pair.with(Pattern.compile("bin\\[(.*)\\]"), s -> String.format("Binary(\"%s\")", s))); add(Pair.with(Pattern.compile("v\\[(.+)\\]\\.id"), s -> world.convertIdToScript(g.V().has("name", s).id().next(), Vertex.class))); @@ -245,7 +251,14 @@ public final class StepDefinition { add(Pair.with(Pattern.compile("uuid\\[(.*)\\]"), UUID::fromString)); add(Pair.with(Pattern.compile("char\\[(.)\\]"), s -> s.charAt(0))); - add(Pair.with(Pattern.compile("dur\\[(.*)\\]"), s -> Duration.parse(s))); + add(Pair.with(Pattern.compile("dur\\[(.*)\\]"), s -> { + final String[] parts = s.split(","); + final long seconds = Long.parseLong(parts[0].trim()); + final int nanos = Integer.parseInt(parts[1].trim()); + final boolean isPositive = parts.length < 3 || Boolean.parseBoolean(parts[2].trim()); + final Duration d = Duration.ofSeconds(seconds, nanos); + return isPositive ? d : d.negated(); + })); add(Pair.with(Pattern.compile("bin\\[(.*)\\]"), s -> ByteBuffer.wrap(Base64.getDecoder().decode(s)))); add(Pair.with(Pattern.compile("v\\[(.+)\\]\\.id"), s -> g.V().has("name", s).id().next())); diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json index 6d5fd5e158..44163ea0f7 100644 --- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json +++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json @@ -3894,146 +3894,98 @@ ] }, { - "scenario": "g_injectXDurationXPT2H30MXX", + "scenario": "g_injectXDurationX9000_0XX", "traversals": [ { - "original": "g.inject(Duration(\"PT2H30M\"))", - "language": "g.inject(Duration(\"PT2H30M\"))", - "canonical": "g.inject(Duration(\"PT2H30M\"))", + "original": "g.inject(Duration(9000, 0))", + "language": "g.inject(Duration(9000,0))", + "canonical": "g.inject(Duration(9000,0))", "anonymized": "g.inject(duration0)", - "dotnet": "g.Inject<object>(XmlConvert.ToTimeSpan(\"PT2H30M\"))", + "dotnet": "g.Inject<object>(TimeSpan.FromTicks(90000000000L))", "go": "g.Inject(time.Duration(9000000000000))", - "groovy": "g.inject(Duration.parse(\"PT2H30M\"))", - "java": "g.inject(Duration.parse(\"PT2H30M\"))", + "groovy": "g.inject(Duration.ofSeconds(9000, 0))", + "java": "g.inject(Duration.ofSeconds(9000, 0))", "python": "g.inject(timedelta(seconds=9000))" } ] }, { - "scenario": "g_injectXDurationXPT0SXX", + "scenario": "g_injectXDurationX0_0XX", "traversals": [ { - "original": "g.inject(Duration(\"PT0S\"))", - "language": "g.inject(Duration(\"PT0S\"))", - "canonical": "g.inject(Duration(\"PT0S\"))", + "original": "g.inject(Duration(0, 0))", + "language": "g.inject(Duration(0,0))", + "canonical": "g.inject(Duration(0,0))", "anonymized": "g.inject(duration0)", - "dotnet": "g.Inject<object>(XmlConvert.ToTimeSpan(\"PT0S\"))", + "dotnet": "g.Inject<object>(TimeSpan.FromTicks(0L))", "go": "g.Inject(time.Duration(0))", - "groovy": "g.inject(Duration.parse(\"PT0S\"))", - "java": "g.inject(Duration.parse(\"PT0S\"))", + "groovy": "g.inject(Duration.ofSeconds(0, 0))", + "java": "g.inject(Duration.ofSeconds(0, 0))", "python": "g.inject(timedelta(seconds=0))" } ] }, { - "scenario": "g_injectXDurationXPTneg30SXX", + "scenario": "g_injectXDurationX0_500000000XX", "traversals": [ { - "original": "g.inject(Duration(\"PT-30S\"))", - "language": "g.inject(Duration(\"PT-30S\"))", - "canonical": "g.inject(Duration(\"PT-30S\"))", + "original": "g.inject(Duration(0, 500000000))", + "language": "g.inject(Duration(0,500000000))", + "canonical": "g.inject(Duration(0,500000000))", "anonymized": "g.inject(duration0)", - "dotnet": "g.Inject<object>(XmlConvert.ToTimeSpan(\"-PT30S\"))", - "go": "g.Inject(time.Duration(-30000000000))", - "groovy": "g.inject(Duration.parse(\"PT-30S\"))", - "java": "g.inject(Duration.parse(\"PT-30S\"))", - "python": "g.inject(timedelta(seconds=-30))" - } - ] - }, - { - "scenario": "g_injectXDurationXnegPT30SXX", - "traversals": [ - { - "original": "g.inject(Duration(\"-PT30S\"))", - "language": "g.inject(Duration(\"-PT30S\"))", - "canonical": "g.inject(Duration(\"-PT30S\"))", - "anonymized": "g.inject(duration0)", - "dotnet": "g.Inject<object>(XmlConvert.ToTimeSpan(\"-PT30S\"))", - "go": "g.Inject(time.Duration(-30000000000))", - "groovy": "g.inject(Duration.parse(\"-PT30S\"))", - "java": "g.inject(Duration.parse(\"-PT30S\"))", - "python": "g.inject(timedelta(seconds=-30))" - } - ] - }, - { - "scenario": "g_injectXDurationXPT0_5SXX", - "traversals": [ - { - "original": "g.inject(Duration(\"PT0.5S\"))", - "language": "g.inject(Duration(\"PT0.5S\"))", - "canonical": "g.inject(Duration(\"PT0.5S\"))", - "anonymized": "g.inject(duration0)", - "dotnet": "g.Inject<object>(XmlConvert.ToTimeSpan(\"PT0.5S\"))", + "dotnet": "g.Inject<object>(TimeSpan.FromTicks(5000000L))", "go": "g.Inject(time.Duration(500000000))", - "groovy": "g.inject(Duration.parse(\"PT0.5S\"))", - "java": "g.inject(Duration.parse(\"PT0.5S\"))", + "groovy": "g.inject(Duration.ofSeconds(0, 500000000))", + "java": "g.inject(Duration.ofSeconds(0, 500000000))", "python": "g.inject(timedelta(seconds=0,microseconds=500000))" } ] }, { - "scenario": "g_injectXDurationXP1DT12HXX", + "scenario": "g_injectXDurationX30_0XX", "traversals": [ { - "original": "g.inject(Duration(\"P1DT12H\"))", - "language": "g.inject(Duration(\"P1DT12H\"))", - "canonical": "g.inject(Duration(\"P1DT12H\"))", + "original": "g.inject(Duration(30, 0))", + "language": "g.inject(Duration(30,0))", + "canonical": "g.inject(Duration(30,0))", "anonymized": "g.inject(duration0)", - "dotnet": "g.Inject<object>(XmlConvert.ToTimeSpan(\"PT36H\"))", - "go": "g.Inject(time.Duration(129600000000000))", - "groovy": "g.inject(Duration.parse(\"P1DT12H\"))", - "java": "g.inject(Duration.parse(\"P1DT12H\"))", - "python": "g.inject(timedelta(seconds=129600))" + "dotnet": "g.Inject<object>(TimeSpan.FromTicks(300000000L))", + "go": "g.Inject(time.Duration(30000000000))", + "groovy": "g.inject(Duration.ofSeconds(30, 0))", + "java": "g.inject(Duration.ofSeconds(30, 0))", + "python": "g.inject(timedelta(seconds=30))" } ] }, { - "scenario": "g_injectXDurationXP2DXX", + "scenario": "g_injectXDurationX30_0_falseXX", "traversals": [ { - "original": "g.inject(Duration(\"P2D\"))", - "language": "g.inject(Duration(\"P2D\"))", - "canonical": "g.inject(Duration(\"P2D\"))", + "original": "g.inject(Duration(30, 0, false))", + "language": "g.inject(Duration(30,0,false))", + "canonical": "g.inject(Duration(30,0,false))", "anonymized": "g.inject(duration0)", - "dotnet": "g.Inject<object>(XmlConvert.ToTimeSpan(\"PT48H\"))", - "go": "g.Inject(time.Duration(172800000000000))", - "groovy": "g.inject(Duration.parse(\"P2D\"))", - "java": "g.inject(Duration.parse(\"P2D\"))", - "python": "g.inject(timedelta(seconds=172800))" - } - ] - }, - { - "scenario": "g_injectXDurationXPT1H30M15SXX", - "traversals": [ - { - "original": "g.inject(Duration(\"PT1H30M15S\"))", - "language": "g.inject(Duration(\"PT1H30M15S\"))", - "canonical": "g.inject(Duration(\"PT1H30M15S\"))", - "anonymized": "g.inject(duration0)", - "dotnet": "g.Inject<object>(XmlConvert.ToTimeSpan(\"PT1H30M15S\"))", - "go": "g.Inject(time.Duration(5415000000000))", - "groovy": "g.inject(Duration.parse(\"PT1H30M15S\"))", - "java": "g.inject(Duration.parse(\"PT1H30M15S\"))", - "python": "g.inject(timedelta(seconds=5415))" + "dotnet": "g.Inject<object>(TimeSpan.FromTicks(300000000L).Negate())", + "go": "g.Inject(time.Duration(-30000000000))", + "groovy": "g.inject(Duration.ofSeconds(30, 0).negated())", + "java": "g.inject(Duration.ofSeconds(30, 0).negated())", + "python": "g.inject(timedelta(seconds=-30))" } ] }, { - "scenario": "g_injectXDurationXPTneg0_5SXX", + "scenario": "g_injectXDurationX1_500000000_falseXX", "traversals": [ { - "original": "g.inject(Duration(\"PT-0.5S\"))", - "language": "g.inject(Duration(\"PT-0.5S\"))", - "canonical": "g.inject(Duration(\"PT-0.5S\"))", + "original": "g.inject(Duration(1, 500000000, false))", + "language": "g.inject(Duration(1,500000000,false))", + "canonical": "g.inject(Duration(1,500000000,false))", "anonymized": "g.inject(duration0)", - "dotnet": "g.Inject<object>(XmlConvert.ToTimeSpan(\"-PT0.5S\"))", - "go": "g.Inject(time.Duration(-500000000))", - "groovy": "g.inject(Duration.parse(\"PT-0.5S\"))", - "java": "g.inject(Duration.parse(\"PT-0.5S\"))", - "python": "g.inject(timedelta(seconds=-1,microseconds=500000))" + "dotnet": "g.Inject<object>(TimeSpan.FromTicks(15000000L).Negate())", + "go": "g.Inject(time.Duration(-1500000000))", + "groovy": "g.inject(Duration.ofSeconds(1, 500000000).negated())", + "java": "g.inject(Duration.ofSeconds(1, 500000000).negated())", + "python": "g.inject(timedelta(seconds=-1,microseconds=-500000))" } ] }, @@ -4041,14 +3993,14 @@ "scenario": "g_valuesXlengthX_isXtypeOfXGType_DURATIONXX", "traversals": [ { - "original": "g.addV(\"data\").property(\"length\", Duration(\"PT2H30M\"))", - "language": "g.addV(\"data\").property(\"length\", Duration(\"PT2H30M\"))", - "canonical": "g.addV(\"data\").property(\"length\", Duration(\"PT2H30M\"))", + "original": "g.addV(\"data\").property(\"length\", Duration(9000, 0))", + "language": "g.addV(\"data\").property(\"length\", Duration(9000,0))", + "canonical": "g.addV(\"data\").property(\"length\", Duration(9000,0))", "anonymized": "g.addV(string0).property(string1, duration0)", - "dotnet": "g.AddV((string) \"data\").Property(\"length\", XmlConvert.ToTimeSpan(\"PT2H30M\"))", + "dotnet": "g.AddV((string) \"data\").Property(\"length\", TimeSpan.FromTicks(90000000000L))", "go": "g.AddV(\"data\").Property(\"length\", time.Duration(9000000000000))", - "groovy": "g.addV(\"data\").property(\"length\", Duration.parse(\"PT2H30M\"))", - "java": "g.addV(\"data\").property(\"length\", Duration.parse(\"PT2H30M\"))", + "groovy": "g.addV(\"data\").property(\"length\", Duration.ofSeconds(9000, 0))", + "java": "g.addV(\"data\").property(\"length\", Duration.ofSeconds(9000, 0))", "python": "g.add_v('data').property('length', timedelta(seconds=9000))" }, { @@ -4066,17 +4018,17 @@ ] }, { - "scenario": "g_injectXDurationXPT2H30MXX_isXgtXDurationXPT1HXXX", + "scenario": "g_injectXDurationX9000_0XX_isXgtXDurationX3600_0XXX", "traversals": [ { - "original": "g.inject(Duration(\"PT2H30M\")).is(P.gt(Duration(\"PT1H\")))", - "language": "g.inject(Duration(\"PT2H30M\")).is(P.gt(Duration(\"PT1H\")))", - "canonical": "g.inject(Duration(\"PT2H30M\")).is(P.gt(Duration(\"PT1H\")))", + "original": "g.inject(Duration(9000, 0)).is(P.gt(Duration(3600, 0)))", + "language": "g.inject(Duration(9000,0)).is(P.gt(Duration(3600,0)))", + "canonical": "g.inject(Duration(9000,0)).is(P.gt(Duration(3600,0)))", "anonymized": "g.inject(duration0).is(P.gt(duration1))", - "dotnet": "g.Inject<object>(XmlConvert.ToTimeSpan(\"PT2H30M\")).Is(P.Gt(XmlConvert.ToTimeSpan(\"PT1H\")))", + "dotnet": "g.Inject<object>(TimeSpan.FromTicks(90000000000L)).Is(P.Gt(TimeSpan.FromTicks(36000000000L)))", "go": "g.Inject(time.Duration(9000000000000)).Is(gremlingo.P.Gt(time.Duration(3600000000000)))", - "groovy": "g.inject(Duration.parse(\"PT2H30M\")).is(P.gt(Duration.parse(\"PT1H\")))", - "java": "g.inject(Duration.parse(\"PT2H30M\")).is(P.gt(Duration.parse(\"PT1H\")))", + "groovy": "g.inject(Duration.ofSeconds(9000, 0)).is(P.gt(Duration.ofSeconds(3600, 0)))", + "java": "g.inject(Duration.ofSeconds(9000, 0)).is(P.gt(Duration.ofSeconds(3600, 0)))", "python": "g.inject(timedelta(seconds=9000)).is_(P.gt(timedelta(seconds=3600)))" } ] diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/data/Duration.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/data/Duration.feature index c9d0a73686..5f2da18e77 100644 --- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/data/Duration.feature +++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/data/Duration.feature @@ -19,118 +19,82 @@ Feature: Data - DURATION @GraphComputerVerificationInjectionNotSupported - Scenario: g_injectXDurationXPT2H30MXX + Scenario: g_injectXDurationX9000_0XX Given the empty graph And the traversal of """ - g.inject(Duration("PT2H30M")) + g.inject(Duration(9000, 0)) """ When iterated to list Then the result should be unordered | result | - | dur[PT2H30M] | + | dur[9000,0] | @GraphComputerVerificationInjectionNotSupported - Scenario: g_injectXDurationXPT0SXX + Scenario: g_injectXDurationX0_0XX Given the empty graph And the traversal of """ - g.inject(Duration("PT0S")) + g.inject(Duration(0, 0)) """ When iterated to list Then the result should be unordered | result | - | dur[PT0S] | + | dur[0,0] | @GraphComputerVerificationInjectionNotSupported - Scenario: g_injectXDurationXPTneg30SXX + Scenario: g_injectXDurationX0_500000000XX Given the empty graph And the traversal of """ - g.inject(Duration("PT-30S")) + g.inject(Duration(0, 500000000)) """ When iterated to list Then the result should be unordered | result | - | dur[PT-30S] | + | dur[0,500000000] | @GraphComputerVerificationInjectionNotSupported - Scenario: g_injectXDurationXnegPT30SXX + Scenario: g_injectXDurationX30_0XX Given the empty graph And the traversal of """ - g.inject(Duration("-PT30S")) + g.inject(Duration(30, 0)) """ When iterated to list Then the result should be unordered | result | - | dur[PT-30S] | + | dur[30,0] | @GraphComputerVerificationInjectionNotSupported - Scenario: g_injectXDurationXPT0_5SXX + Scenario: g_injectXDurationX30_0_falseXX Given the empty graph And the traversal of """ - g.inject(Duration("PT0.5S")) + g.inject(Duration(30, 0, false)) """ When iterated to list Then the result should be unordered | result | - | dur[PT0.5S] | + | dur[30,0,false] | @GraphComputerVerificationInjectionNotSupported - Scenario: g_injectXDurationXP1DT12HXX + Scenario: g_injectXDurationX1_500000000_falseXX Given the empty graph And the traversal of """ - g.inject(Duration("P1DT12H")) + g.inject(Duration(1, 500000000, false)) """ When iterated to list Then the result should be unordered | result | - | dur[PT36H] | - - @GraphComputerVerificationInjectionNotSupported - Scenario: g_injectXDurationXP2DXX - Given the empty graph - And the traversal of - """ - g.inject(Duration("P2D")) - """ - When iterated to list - Then the result should be unordered - | result | - | dur[PT48H] | - - @GraphComputerVerificationInjectionNotSupported - Scenario: g_injectXDurationXPT1H30M15SXX - Given the empty graph - And the traversal of - """ - g.inject(Duration("PT1H30M15S")) - """ - When iterated to list - Then the result should be unordered - | result | - | dur[PT1H30M15S] | - - @GraphComputerVerificationInjectionNotSupported - Scenario: g_injectXDurationXPTneg0_5SXX - Given the empty graph - And the traversal of - """ - g.inject(Duration("PT-0.5S")) - """ - When iterated to list - Then the result should be unordered - | result | - | dur[PT-0.5S] | + | dur[1,500000000,false] | Scenario: g_valuesXlengthX_isXtypeOfXGType_DURATIONXX Given the empty graph And the graph initializer of """ - g.addV("data").property("length", Duration("PT2H30M")) + g.addV("data").property("length", Duration(9000, 0)) """ And the traversal of """ @@ -139,16 +103,16 @@ Feature: Data - DURATION When iterated to list Then the result should be unordered | result | - | dur[PT2H30M] | + | dur[9000,0] | @GraphComputerVerificationInjectionNotSupported - Scenario: g_injectXDurationXPT2H30MXX_isXgtXDurationXPT1HXXX + Scenario: g_injectXDurationX9000_0XX_isXgtXDurationX3600_0XXX Given the empty graph And the traversal of """ - g.inject(Duration("PT2H30M")).is(P.gt(Duration("PT1H"))) + g.inject(Duration(9000, 0)).is(P.gt(Duration(3600, 0))) """ When iterated to list Then the result should be unordered | result | - | dur[PT2H30M] | + | dur[9000,0] |
