[ 
https://issues.apache.org/jira/browse/TINKERPOP-2334?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=17779183#comment-17779183
 ] 

ASF GitHub Bot commented on TINKERPOP-2334:
-------------------------------------------

Cole-Greer commented on code in PR #2307:
URL: https://github.com/apache/tinkerpop/pull/2307#discussion_r1370523821


##########
gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/FormatStep.java:
##########
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tinkerpop.gremlin.process.traversal.step.map;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.tinkerpop.gremlin.process.traversal.Pop;
+import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
+import org.apache.tinkerpop.gremlin.process.traversal.Traverser;
+import org.apache.tinkerpop.gremlin.process.traversal.step.PathProcessor;
+import org.apache.tinkerpop.gremlin.process.traversal.step.Scoping;
+import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent;
+import 
org.apache.tinkerpop.gremlin.process.traversal.traverser.TraverserRequirement;
+import 
org.apache.tinkerpop.gremlin.process.traversal.traverser.util.EmptyTraverser;
+import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalProduct;
+import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalRing;
+import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalUtil;
+import org.apache.tinkerpop.gremlin.structure.Element;
+import org.apache.tinkerpop.gremlin.structure.Property;
+import org.apache.tinkerpop.gremlin.structure.util.StringFactory;
+
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Reference implementation for String format step, a mid-traversal step which 
will handle result formatting
+ * to string values. If the incoming traverser is a non-String value then an 
{@code IllegalArgumentException}
+ * will be thrown.
+ *
+ * @author Valentyn Kahamlyk
+ */
+public final class FormatStep<S> extends MapStep<S, String> implements 
TraversalParent, Scoping, PathProcessor {
+
+    private String format;
+    private Set<String> variables;
+    private TraversalRing<S, String> traversalRing = new TraversalRing<>();
+    private Set<String> keepLabels;
+
+    public FormatStep(final Traversal.Admin traversal, final String format) {
+        super(traversal);
+        this.format = format;
+        this.variables = getVariables();
+    }
+
+    @Override
+    protected Traverser.Admin<String> processNextStart() {
+        final Map<String, Object> values = new HashMap<>();
+
+        final Traverser.Admin traverser = this.starts.next();
+
+        boolean productive = true;
+        for (final String var : variables) {
+            final Object current = traverser.get();
+            // try to get property value
+            if (current instanceof Element) {
+                final Property prop = ((Element) current).property(var);
+                if (prop != null && prop.isPresent()) {
+                    values.put(var, prop.value());
+                    continue;
+                }
+            }
+
+            final TraversalProduct product =
+                    TraversalUtil.produce((S) 
this.getNullableScopeValue(Pop.last, var, traverser), 
this.traversalRing.next());
+
+            if (!product.isProductive() || product.get() == null) {
+                productive = false;
+                break;
+            }
+
+            values.put(var, product.get());
+        }
+        this.traversalRing.reset();
+
+        return productive ?
+                
PathProcessor.processTraverserPathLabels(traverser.split(evaluate(values), 
this), this.keepLabels) :
+                EmptyTraverser.instance();
+    }
+
+    @Override
+    public String toString() {
+        return StringFactory.stepString(this, this.format, this.traversalRing);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = super.hashCode();
+        return Objects.hash(result, format, traversalRing);
+    }
+
+    @Override
+    public List<Traversal.Admin<S, String>> getLocalChildren() {
+        return this.traversalRing.getTraversals();
+    }
+
+    @Override
+    public void reset() {
+        super.reset();
+        this.traversalRing.reset();
+    }
+
+    @Override
+    public FormatStep<S> clone() {
+        final FormatStep<S> clone = (FormatStep<S>) super.clone();
+        clone.format = this.format;
+        clone.variables = this.variables;
+        clone.traversalRing = this.traversalRing;
+        return clone;
+    }
+
+    @Override
+    public void setTraversal(final Traversal.Admin<?, ?> parentTraversal) {
+        super.setTraversal(parentTraversal);
+        this.traversalRing.getTraversals().forEach(this::integrateChild);
+    }
+
+    @Override
+    public Set<TraverserRequirement> getRequirements() {
+        return this.getSelfAndChildRequirements(TraverserRequirement.OBJECT, 
TraverserRequirement.SIDE_EFFECTS);
+    }
+
+    @Override
+    public Set<String> getScopeKeys() {
+        return variables;
+    }
+
+    @Override
+    public void setKeepLabels(final Set<String> labels) {
+        this.keepLabels = labels;
+    }
+
+    @Override
+    public Set<String> getKeepLabels() {
+        return this.keepLabels;
+    }
+
+    // private methods
+
+    private static final Pattern VARIABLE_PATTERN = 
Pattern.compile("%\\{(.*?)\\}");
+
+    Set<String> getVariables() {
+        final Matcher matcher = VARIABLE_PATTERN.matcher(format);
+        final Set<String> variables = new LinkedHashSet<>();
+        while (matcher.find()) {
+            final String varName = matcher.group(1);
+            if (!StringUtils.isEmpty(varName)) {
+                variables.add(matcher.group(1));
+            }
+        }
+        return variables;
+    }
+
+    private String evaluate(final Map<String, Object> values) {
+        int lastIndex = 0;
+        final StringBuilder output = new StringBuilder();
+        final Matcher matcher = VARIABLE_PATTERN.matcher(format);
+        matcher.reset();
+
+        while (matcher.find()) {
+            final String varName = matcher.group(1);
+            if (StringUtils.isEmpty(varName)) continue;

Review Comment:
   I think an empty string should be a valid varName based on this:
   ```
   gremlin> g.inject("test").as("").V(1).select("")
   ==>test
   ```



##########
gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/FormatStepTest.java:
##########
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tinkerpop.gremlin.process.traversal.step.map;
+
+import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
+import org.apache.tinkerpop.gremlin.process.traversal.step.StepTest;
+import org.apache.tinkerpop.gremlin.structure.Vertex;
+import org.apache.tinkerpop.gremlin.structure.VertexProperty;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.apache.tinkerpop.gremlin.util.CollectionUtil.asMap;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class FormatStepTest extends StepTest {
+
+    @Override
+    protected List<Traversal> getTraversals() {
+        return Collections.singletonList(
+                __.format("hello")
+        );
+    }
+
+    private List<String> getVariables(final String format) {
+        final FormatStep formatStep = new 
FormatStep(__.inject("test").asAdmin(), format);
+        return new ArrayList<>(formatStep.getScopeKeys());
+    }
+
+    @Test
+    public void shouldGetVariablesFromTemplate() {
+        assertEquals(Collections.emptyList(), getVariables("Hello world"));
+        assertEquals(Collections.emptyList(), getVariables("Hello %{world"));
+        assertEquals(Collections.emptyList(), getVariables("Hello %{}"));
+        assertEquals(Collections.emptyList(), getVariables("Hello {world}"));
+        assertEquals(Collections.emptyList(), getVariables("Hello % {world}"));
+        assertEquals(Collections.singletonList(" "), getVariables("Hello %{ 
}"));
+        assertEquals(Collections.singletonList("world"), getVariables("Hello 
%{world}"));
+        assertEquals(Arrays.asList("Hello", "world"), getVariables("%{Hello} 
%{world}"));
+        assertEquals(Arrays.asList("Hello", "world"), getVariables("%{Hello} 
%{Hello} %{world}"));
+        assertEquals(Arrays.asList("Hello", "hello", "world"), 
getVariables("%{Hello} %{hello} %{world}"));
+    }
+
+    @Test
+    public void shouldWorkWithoutVariables() {
+        assertEquals("Hello world", __.__("test").format("Hello 
world").next());
+    }
+
+    @Test
+    public void shouldWorkWithVertexInput() {
+        final VertexProperty mockProperty = mock(VertexProperty.class);
+        when(mockProperty.key()).thenReturn("name");
+        when(mockProperty.value()).thenReturn("Stephen");
+        when(mockProperty.isPresent()).thenReturn(true);
+
+        final Vertex mockVertex = mock(Vertex.class);
+        when(mockVertex.property("name")).thenReturn(mockProperty);
+
+        assertEquals("Hello Stephen", __.__(mockVertex).format("Hello 
%{name}").next());
+    }
+
+    @Test
+    public void shouldWorkWithMultipleVertexInput() {
+        final VertexProperty mockProperty1 = mock(VertexProperty.class);
+        when(mockProperty1.key()).thenReturn("name");
+        when(mockProperty1.value()).thenReturn("Stephen");
+        when(mockProperty1.isPresent()).thenReturn(true);
+
+        final Vertex mockVertex1 = mock(Vertex.class);
+        when(mockVertex1.property("name")).thenReturn(mockProperty1);
+
+        final VertexProperty mockProperty2 = mock(VertexProperty.class);
+        when(mockProperty2.key()).thenReturn("name");
+        when(mockProperty2.value()).thenReturn("Marko");
+        when(mockProperty2.isPresent()).thenReturn(true);
+
+        final Vertex mockVertex2 = mock(Vertex.class);
+        when(mockVertex2.property("name")).thenReturn(mockProperty2);
+
+        assertEquals(Arrays.asList("Hello Stephen", "Hello Marko"),
+                __.__(mockVertex1, mockVertex2).format("Hello 
%{name}").toList());
+    }
+    @Test
+    public void shouldWorkWithMap() {
+        assertEquals("Hello 2", __.__(asMap("name", 2)).format("Hello 
%{name}").next());
+        assertEquals("Hello Stephen", __.__(asMap("name", 
"Stephen")).format("Hello %{name}").next());
+    }
+
+    @Test
+    public void shouldWorkWithScopeVariables() {
+         assertEquals("Hello Stephen", 
__.__("Stephen").as("name").format("Hello %{name}").next());
+    }
+
+    @Test
+    public void shouldHandleSameVariableTwice() {
+        assertEquals("Hello, Hello Stephen",
+                __.__("Hello").as("action").format("%{action}, %{action} 
Stephen").next());
+    }
+
+    @Test
+    public void shouldWorkWithMixedInput() {
+        final VertexProperty mockProperty1 = mock(VertexProperty.class);
+        when(mockProperty1.key()).thenReturn("p1");
+        when(mockProperty1.value()).thenReturn("val1");
+        when(mockProperty1.isPresent()).thenReturn(true);
+
+        final VertexProperty mockProperty2 = mock(VertexProperty.class);
+        when(mockProperty2.key()).thenReturn("p2");
+        when(mockProperty2.value()).thenReturn("val2");
+        when(mockProperty2.isPresent()).thenReturn(true);
+
+        final Vertex mockVertex = mock(Vertex.class);
+        when(mockVertex.property("p1")).thenReturn(mockProperty1);
+        when(mockVertex.property("p2")).thenReturn(mockProperty2);
+
+        assertEquals("val1 val2 valA valB",
+                __.inject("valA").as("varA").
+                        constant("valB").as("varB").
+                        constant(mockVertex).format("%{p1} %{p2} %{varA} 
%{varB}").next());
+    }

Review Comment:
   I'm a bit concerned about namespace collisions between property keys and 
scope variables. Could you add a test which has a name collision? Based on the 
semantics docs, I would expect the following:
   ```
   gremlin> g.inject("stephen").as("name").V(1).format("Hello %{name}")
   ==>Hello marko
   ```



##########
gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/FormatStepTest.java:
##########
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tinkerpop.gremlin.process.traversal.step.map;
+
+import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
+import org.apache.tinkerpop.gremlin.process.traversal.step.StepTest;
+import org.apache.tinkerpop.gremlin.structure.Vertex;
+import org.apache.tinkerpop.gremlin.structure.VertexProperty;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.apache.tinkerpop.gremlin.util.CollectionUtil.asMap;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class FormatStepTest extends StepTest {
+
+    @Override
+    protected List<Traversal> getTraversals() {
+        return Collections.singletonList(
+                __.format("hello")
+        );
+    }
+
+    private List<String> getVariables(final String format) {
+        final FormatStep formatStep = new 
FormatStep(__.inject("test").asAdmin(), format);
+        return new ArrayList<>(formatStep.getScopeKeys());
+    }
+
+    @Test
+    public void shouldGetVariablesFromTemplate() {
+        assertEquals(Collections.emptyList(), getVariables("Hello world"));
+        assertEquals(Collections.emptyList(), getVariables("Hello %{world"));
+        assertEquals(Collections.emptyList(), getVariables("Hello %{}"));
+        assertEquals(Collections.emptyList(), getVariables("Hello {world}"));
+        assertEquals(Collections.emptyList(), getVariables("Hello % {world}"));
+        assertEquals(Collections.singletonList(" "), getVariables("Hello %{ 
}"));
+        assertEquals(Collections.singletonList("world"), getVariables("Hello 
%{world}"));
+        assertEquals(Arrays.asList("Hello", "world"), getVariables("%{Hello} 
%{world}"));
+        assertEquals(Arrays.asList("Hello", "world"), getVariables("%{Hello} 
%{Hello} %{world}"));
+        assertEquals(Arrays.asList("Hello", "hello", "world"), 
getVariables("%{Hello} %{hello} %{world}"));
+    }
+
+    @Test
+    public void shouldWorkWithoutVariables() {
+        assertEquals("Hello world", __.__("test").format("Hello 
world").next());
+    }
+
+    @Test
+    public void shouldWorkWithVertexInput() {
+        final VertexProperty mockProperty = mock(VertexProperty.class);
+        when(mockProperty.key()).thenReturn("name");
+        when(mockProperty.value()).thenReturn("Stephen");
+        when(mockProperty.isPresent()).thenReturn(true);
+
+        final Vertex mockVertex = mock(Vertex.class);
+        when(mockVertex.property("name")).thenReturn(mockProperty);
+
+        assertEquals("Hello Stephen", __.__(mockVertex).format("Hello 
%{name}").next());
+    }
+
+    @Test
+    public void shouldWorkWithMultipleVertexInput() {
+        final VertexProperty mockProperty1 = mock(VertexProperty.class);
+        when(mockProperty1.key()).thenReturn("name");
+        when(mockProperty1.value()).thenReturn("Stephen");
+        when(mockProperty1.isPresent()).thenReturn(true);
+
+        final Vertex mockVertex1 = mock(Vertex.class);
+        when(mockVertex1.property("name")).thenReturn(mockProperty1);
+
+        final VertexProperty mockProperty2 = mock(VertexProperty.class);
+        when(mockProperty2.key()).thenReturn("name");
+        when(mockProperty2.value()).thenReturn("Marko");
+        when(mockProperty2.isPresent()).thenReturn(true);
+
+        final Vertex mockVertex2 = mock(Vertex.class);
+        when(mockVertex2.property("name")).thenReturn(mockProperty2);
+
+        assertEquals(Arrays.asList("Hello Stephen", "Hello Marko"),
+                __.__(mockVertex1, mockVertex2).format("Hello 
%{name}").toList());
+    }
+    @Test
+    public void shouldWorkWithMap() {
+        assertEquals("Hello 2", __.__(asMap("name", 2)).format("Hello 
%{name}").next());
+        assertEquals("Hello Stephen", __.__(asMap("name", 
"Stephen")).format("Hello %{name}").next());
+    }
+
+    @Test
+    public void shouldWorkWithScopeVariables() {
+         assertEquals("Hello Stephen", 
__.__("Stephen").as("name").format("Hello %{name}").next());
+    }
+
+    @Test
+    public void shouldHandleSameVariableTwice() {
+        assertEquals("Hello, Hello Stephen",
+                __.__("Hello").as("action").format("%{action}, %{action} 
Stephen").next());
+    }
+

Review Comment:
   Could you add a test for undefined variables being filtered out?
   ```
   gremlin> g.V(1).format("%{name}'s birthday is %{birthday}!")
   //No results
   ```



##########
docs/src/dev/provider/gremlin-semantics.asciidoc:
##########
@@ -892,6 +892,36 @@ None
 
 None
 
+[[format-step]]
+=== format()
+
+*Description:* a mid-traversal step which will handle result formatting to 
string values.
+
+*Syntax:* `format(String)`
+
+[width="100%",options="header"]
+|=========================================================
+|Start Step |Mid Step |Modulated |Domain |Range
+|N |Y |N |`any` |`String`
+|=========================================================
+
+*Arguments:*
+
+Format string. Variables can be represented using `%{variable_name}` notation.
+The variable values are used in the order that the first one will be found: 
Element properties, than Scope values.
+If value for some variable was not found than result for input is skipped.

Review Comment:
   I prefer `filtered` over `skipped` here. To me, skipped is ambiguous if it 
means the whole string result is skipped, or if that single variable is left 
blank in the result.
   ```suggestion
   The variable values are used in the order that the first one will be found: 
Element properties, then Scope values.
   If value for some variable was not found, then the result is filtered out.
   ```





> Add format() step
> -----------------
>
>                 Key: TINKERPOP-2334
>                 URL: https://issues.apache.org/jira/browse/TINKERPOP-2334
>             Project: TinkerPop
>          Issue Type: Improvement
>          Components: process
>    Affects Versions: 3.4.4
>            Reporter: Stephen Mallette
>            Assignee: Valentyn Kahamlyk
>            Priority: Major
>
> Provide for a {{format()}} step which will handle result formatting to string 
> values. This change will help resolve the need for string concatenation 
> functions while providing a lot of flexibility to how results can be formed:
> {code}
> gremlin> g.V().hasLabel('person').format("%{n} is %{a} years old.").by('n',
> 'name').by('a', 'age')
> ==>marko is 29 years old.
> ==>vadas is 27 years old.
> ==>josh is 32 years old.
> ==>peter is 35 years old.
> gremlin> g.V().hasLabel('person').format("%{name} is %{age} years old.")
> ==>marko is 29 years old.
> ==>vadas is 27 years old.
> ==>josh is 32 years old.
> ==>peter is 35 years old.
> {code}



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to