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


The following commit(s) were added to refs/heads/stringify-params by this push:
     new a093f4df18 rework dotnet translations
a093f4df18 is described below

commit a093f4df1854e09f606c3b47ea4011c8d14ddbf2
Author: Ken Hu <[email protected]>
AuthorDate: Wed Apr 22 21:02:21 2026 -0700

    rework dotnet translations
---
 .../translator/DotNetTranslateVisitor.java         | 13 +++-
 .../language/translator/GremlinTranslatorTest.java |  9 +++
 .../Gherkin/CommonSteps.cs                         | 11 +++-
 .../Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs |  8 +--
 .../language/translator/DotNetTranslateVisitor.ts  | 72 +++++++++++++++++++++-
 .../unit/translator/gremlin-translator-test.js     |  2 +
 .../gremlin/language/translator/translations.json  |  8 +--
 7 files changed, 108 insertions(+), 15 deletions(-)

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 f80123c4da..663547b520 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,9 +1195,16 @@ public class DotNetTranslateVisitor extends 
AbstractTranslateVisitor {
 
     @Override
     public Void visitDurationLiteral(final 
GremlinParser.DurationLiteralContext ctx) {
-        sb.append("XmlConvert.ToTimeSpan(");
-        sb.append(ctx.stringLiteral().getText());
-        sb.append(")");
+        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("\")");
         return null;
     }
 
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 cb97ae615d..a8792c2b5a 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
@@ -1444,6 +1444,15 @@ public class GremlinTranslatorTest {
                             "g.inject(Duration.parse(\"PT2H30M\"))",
                             "Duration literals are not supported in 
JavaScript",
                             "g.inject(timedelta(seconds=9000))"},
+                    {"g.inject(Duration(\"PT2H-30M\"))",
+                            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\"))",
+                            "Duration literals are not supported in 
JavaScript",
+                            "g.inject(timedelta(seconds=5400))"},
                     {"g.inject(Binary(\"AQID\"))",
                             null,
                             "g.inject(bytebuffer0)",
diff --git 
a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs 
b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs
index 203317c18d..65e7f9fa51 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/CommonSteps.cs
@@ -551,7 +551,16 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
 
         private static object ToDuration(string iso8601, string graphName)
         {
-            return System.Xml.XmlConvert.ToTimeSpan(iso8601);
+            // 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 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 25c155d360..392d7aa296 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
@@ -266,13 +266,13 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
                {"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("PT-30S"))}}, 
+               {"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("P1DT12H"))}}, 
-               {"g_injectXDurationXP2DXX", new List<Func<GraphTraversalSource, 
IDictionary<string, object>, ITraversal>> {(g,p) 
=>g.Inject<object>(XmlConvert.ToTimeSpan("P2D"))}}, 
+               {"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("PT-0.5S"))}}, 
+               {"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_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))}},
 
diff --git 
a/gremlin-js/gremlin-javascript/lib/language/translator/DotNetTranslateVisitor.ts
 
b/gremlin-js/gremlin-javascript/lib/language/translator/DotNetTranslateVisitor.ts
index 247f54af3f..8c4b6be3fc 100644
--- 
a/gremlin-js/gremlin-javascript/lib/language/translator/DotNetTranslateVisitor.ts
+++ 
b/gremlin-js/gremlin-javascript/lib/language/translator/DotNetTranslateVisitor.ts
@@ -235,9 +235,33 @@ export default class DotNetTranslateVisitor extends 
TranslateVisitor {
     }
 
     visitDurationLiteral(ctx: any): void {
-        this.sb.push('XmlConvert.ToTimeSpan(');
-        this.sb.push(ctx.stringLiteral().getText());
-        this.sb.push(')');
+        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`;
+            }
+        }
+        if (normalized === 'PT') normalized = 'PT0S';
+        if (isNegative) normalized = '-' + normalized;
+        this.sb.push(`XmlConvert.ToTimeSpan("${normalized}")`);
     }
 
     visitBinaryLiteral(ctx: any): void {
@@ -866,6 +890,48 @@ 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/test/unit/translator/gremlin-translator-test.js 
b/gremlin-js/gremlin-javascript/test/unit/translator/gremlin-translator-test.js
index 4fddec1343..d17f1a7f85 100644
--- 
a/gremlin-js/gremlin-javascript/test/unit/translator/gremlin-translator-test.js
+++ 
b/gremlin-js/gremlin-javascript/test/unit/translator/gremlin-translator-test.js
@@ -373,6 +373,8 @@ describe('DotNetTranslateVisitor', function () {
       ['g.inject("a"c)', "g.Inject<object>('a')"],
       // Duration literal
       ['g.inject(Duration("PT2H30M"))', 
'g.Inject<object>(XmlConvert.ToTimeSpan("PT2H30M"))'],
+      // Duration literal - mixed sign (2h minus 30m = 1h30m)
+      ['g.inject(Duration("PT2H-30M"))', 
'g.Inject<object>(XmlConvert.ToTimeSpan("PT1H30M"))'],
       // Binary literal
       ['g.inject(Binary("AQID"))', 
'g.Inject<object>(Convert.FromBase64String("AQID"))'],
       // Map literal
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 d22dd6c53f..6d5fd5e158 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
@@ -3933,7 +3933,7 @@
                 "language": "g.inject(Duration(\"PT-30S\"))",
                 "canonical": "g.inject(Duration(\"PT-30S\"))",
                 "anonymized": "g.inject(duration0)",
-                "dotnet": 
"g.Inject<object>(XmlConvert.ToTimeSpan(\"PT-30S\"))",
+                "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\"))",
@@ -3981,7 +3981,7 @@
                 "language": "g.inject(Duration(\"P1DT12H\"))",
                 "canonical": "g.inject(Duration(\"P1DT12H\"))",
                 "anonymized": "g.inject(duration0)",
-                "dotnet": 
"g.Inject<object>(XmlConvert.ToTimeSpan(\"P1DT12H\"))",
+                "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\"))",
@@ -3997,7 +3997,7 @@
                 "language": "g.inject(Duration(\"P2D\"))",
                 "canonical": "g.inject(Duration(\"P2D\"))",
                 "anonymized": "g.inject(duration0)",
-                "dotnet": "g.Inject<object>(XmlConvert.ToTimeSpan(\"P2D\"))",
+                "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\"))",
@@ -4029,7 +4029,7 @@
                 "language": "g.inject(Duration(\"PT-0.5S\"))",
                 "canonical": "g.inject(Duration(\"PT-0.5S\"))",
                 "anonymized": "g.inject(duration0)",
-                "dotnet": 
"g.Inject<object>(XmlConvert.ToTimeSpan(\"PT-0.5S\"))",
+                "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\"))",

Reply via email to