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\"))",