This is an automated email from the ASF dual-hosted git repository.
bechbd pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
The following commit(s) were added to refs/heads/master by this push:
new 2ca96cd8c8 Proposal 3 - Replace Closures with String/List/Date
functions (#1977)
2ca96cd8c8 is described below
commit 2ca96cd8c87062036ca547911075d4f212eee44f
Author: Dave Bechberger <[email protected]>
AuthorDate: Wed Mar 8 17:20:30 2023 -0900
Proposal 3 - Replace Closures with String/List/Date functions (#1977)
* Added Proposal 3 for removing the need for closures in Gremlin
---
docs/src/dev/future/index.asciidoc | 1 +
.../dev/future/proposal-3-remove-closures.asciidoc | 1513 ++++++++++++++++++++
2 files changed, 1514 insertions(+)
diff --git a/docs/src/dev/future/index.asciidoc
b/docs/src/dev/future/index.asciidoc
index a89f9bbaaf..f849877b4a 100644
--- a/docs/src/dev/future/index.asciidoc
+++ b/docs/src/dev/future/index.asciidoc
@@ -97,6 +97,7 @@ story.
|Proposal |Description |Targets |Resolved
|link:https://github.com/apache/tinkerpop/blob/master/docs/src/dev/future/proposal-equality-1.asciidoc[Proposal
1] |Equality, Equivalence, Comparability and Orderability Semantics -
Documents existing Gremlin semantics along with clarifications for ambiguous
behaviors and recommendations for consistency. |3.6.0 |N
|link:https://github.com/apache/tinkerpop/blob/master/docs/src/dev/future/proposal-arrow-flight-2[Proposal
2] |Gremlin Arrow Flight. |4.0.0 |N
+|link:https://github.com/apache/tinkerpop/blob/master/docs/src/dev/future/proposal-3-remove-closures[Proposal
3] |Removing the Need for Closures/Lambda in Gremlin |3.7.0 |N
|=========================================================
= Appendix
diff --git a/docs/src/dev/future/proposal-3-remove-closures.asciidoc
b/docs/src/dev/future/proposal-3-remove-closures.asciidoc
new file mode 100644
index 0000000000..3ab694ed08
--- /dev/null
+++ b/docs/src/dev/future/proposal-3-remove-closures.asciidoc
@@ -0,0 +1,1513 @@
+////
+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.
+////
+== Proposal 3 - Removing the Need for Closures/Lambda in Gremlin
+
+=== Status
+
+This proposal has been accepted through a post to the TinkerPop Dev List and
is ready to begin implementaion.
+
+=== Motivation
+
+There are a number of useful operations that Gremlin users often wish to
+perform that are not provided today in the form of traversal steps or
+predicates (P/TextP). For historical reasons these functions were
+omitted and users were able to accomplish these tasks by specifying
+anonymous code blocks or “closures” to perform these tasks. For example,
+below is an example of how you can achieve a case-insensitive search for
+any cities that contain “Miami”.
+
+....
+g.V().hasLabel('city').
+ has('name',filter{it.get().toLowerCase().contains('Miami'.toLowerCase())})
+....
+
+While this is just one example of how closures are used, they are a
+powerful fallback mechanism in Gremlin to handle use cases where there
+is no functionality within the Gremlin language to meet the
+requirements. However, for a variety of reasons such as security and
+performance, many/most remote providers of TinkerPop do not allow users
+to execute closures as part of a query. This leaves users with a
+problem, as the mechanism provided to solve these sorts of use cases is
+not allowed. Examples of some commonly requested functionality that
+cannot be accomplished without the use of closures would be:
+
+[cols=",,",options="header",]
+|===
+|String Functions |List Functions |Date Functions
+|asString |reverse |dateAdd
+|concat |remove |dateDiff
+|length |indexOf |asDate
+|split |product |
+|substring |all |
+|rTrim |any |
+|lTrim |none |
+|trim |concat |
+|replace |length |
+|reverse |intersect |
+|toUpper |difference |
+|toLower |union |
+|===
+
+=== Considerations
+
+* Adding full support for traversals as parameters to predicates would
+simplify the syntax of these examples. However, this is a known issue
+with Gremlin and none of the proposed options below are blocked by it,
+nor do they exacerbated this issue any further. As such, the
+ramifications of that change are not covered by this proposal.
+
+=== Proposed Options and Recommendation
+
+==== Option 1 (Recommended)
+
+Create a new Gremlin step for each of the desired functions a user is
+looking to perform. Each step encapsulates a set of functionality that
+customers are looking to achieve. While certain steps may be reused
+across input types (e.g. `reverse()` for both list and string inputs)
+the behaviors of each step is well-defined for a given input type.
+
+*Example*: Find me all `city` nodes with a `name` starting with `miami`,
+ignoring case?
+`g.V().hasLabel('city').where(values('name').toLower(), eq('miami'))`
+
+===== Pros:
+
+* Most similar to current Gremlin patterns for adding steps
+* Feel the most like Gremlin when writing the
+
+===== Cons:
+
+* Would result in a potentially large number of steps being added to the
+language, hindering discoverability
+* Adds complexity to creating and maintaining all the current GLVs due
+to the number of new steps.
+
+==== Option 2
+
+Create a single Gremlin step that take the operation as a parameter and
+uses that parameter to mutate the behavior to achieve the desired
+functionality.
+
+*Example*: : Find me all `city` nodes with a `name` starting with
+`miami`, ignoring case?
+`g.V().hasLabel('city').where(F.apply(Func.toLower, values('name')),
eq('miami'))`
+
+===== Pros:
+
+* Single step simplifies adding new operations across GLVs
+
+===== Cons:
+
+* Introduces a novel concept, a function, to the Gremlin language
+* No set signature for `F.apply` as it will differ per operation
+
+==== Option 3
+
+Create a new type of predicate in Gremlin that specifies the operation
+and returns the correct output. This would be a new paradigm within
+Gremlin, as it extends predicates to return non-boolean results.
+
+*Example*:: Find me all `city` nodes with a `name` starting with
+`miami`, ignoring case?
+`g.V().hasLabel('city').where(SP.toLower(values('name')).is('miami'))`
+
+===== Pros:
+
+* Single predicate simplifies adding new operations as they are now a
+token and not a new step that needs to be propagated across all the GLVs
+
+===== Cons:
+
+* New paradigm in Gremlin that further blurs the line between predicates
+and steps
+* Signature differs for each predicate operation
+* Signatures of common steps needs to change to include options like
+`where(Predicate)`
+
+=== Proposed Syntax
+
+<<string-function-syntax>>
+
+<<list-function-syntax>>
+
+<<date-function-syntax>>
+
+=== Examples
+
+==== String Examples
+
+===== String Example 1 (SE1)
+
+I want to find the offices, by name, where the name does not have a "-"
+as the third character of the string
+(https://stackoverflow.com/questions/56115935/gremlin-is-there-a-way-to-find-the-character-based-on-the-index-of-a-string[here])
+
+`g.V().hasLabel('office').where(__.values('name').substr(2, 1)).is(neq('-'))) `
+
+===== String Example 2 (SE2)
+
+I would like to trim out the "Mbit/s" from the string
+(https://stackoverflow.com/questions/45365726/im-unable-to-substring-values-that-i-get-by-running-a-gremlin-query-ive-been[here])
+
+`g.V('Service').has('serviceId','ETHA12819844').out('AssociatedToService').`
+`value("bandwidth").replace("Mbit/s", "")`
+
+===== String Example 3 (SE3)
+
+I am trying to add a new vertex which should be labeled like an existing
+vertex but with some prefix attached
+(https://stackoverflow.com/questions/61106927/concatenate-gremlin-graphtraversal-result-with-string[here])
+
+....
+`g.V(3).as('a').addV(constant("").concat("prefix_", select('a').label())`
+....
+
+===== String Example 4 (SE4)
+
+Find all products that start with the same case-insensitive prefix. +
+e.g. Given the following products:
+
+[cols=",",options="header",]
+|===
+|id |product_name
+|1 |PROD-123
+|2 |PROD-234
+|3 |TEST-1234
+|4 |GAMMA-1234
+|5 |PR-123
+|===
+
+We should return:
+
+[cols=",",options="header",]
+|===
+|id |product_name
+|1 |PROD-123
+|2 |PROD-234
+|===
+
+....
+g.V().hasLabel('Product').has('product_name').as('product1').
+ V().hasLabel('Product').has('product_name'`).`
+
where(__.is(select('product1').toLower())`.values('product_name').substring(0,
5)).
+ select('product1')
+....
+
+===== String Example 5 (SE5)
+
+Perform case-insensitive search
+
+....
+g.V().hasLabel('Product').where(values('product_name').toLower(), eq('foo'))
+....
+
+===== String Example 6 (SE6)
+
+Applying functions to returning values, in this case return the `age`
+and a lower cased version of `name`
+
+`g.V().hasLabel('person').valueMap('age', 'name').by().by(toLower())`
+
+===== String Example 7 (SE7)
+
+Concatenating values on the return, in this case return a concatenated
+name
+
+`g.V().hasLabel('person').project('age', 'name').` `by('age').`
+`by(values('first_name').concat(" ").concat(values('last_name'))`
+
+==== List Examples
+
+===== List Example 1 (LE1)
+
+Given a list of people, return the list of `age`s if everyone’s `age` >
+18
+
+`g.V().hasLabel('person').values('age').fold().where(all(gt(18)))`
+
+===== List Example 2 (LE2)
+
+Given a set of vertices, return the list of vertices if anyone’s `age` >
+18
+
+`g.V([1,2,3,4]).fold().where(any(values('age').is(gt(18))))`
+
+===== List Example 3 (LE3)
+
+Given a list, find the index of the first occurrence of `Dave`
+
+`g.V().hasLabel('person').fold().indexOf(has('name', 'Dave'))` `==> 12`
+
+`g.inject(['Dave', 'Kelvin', 'Stephen']).indexOf(constant('Dave'))`
+`==> 0`
+
+===== List Example 4 (LE4)
+
+Given a list of people, remove any person with a name of `Dave`
+
+`g.V().hasLabel('person').fold().remove(has('name', 'Dave'))`
+`==> [‘Kelvin’, ‘Stephen’]`
+
+`g.inject(['Dave', 'Kelvin', 'Stephen']).remove(constant('Dave'))`
+`==> [‘Kelvin’, ‘Stephen’]`
+
+`g.inject(['Dave', 'Kelvin', 'Stephen']).remove(constant(['Dave', 'Stephen'))`
+`==> ['Kelvin']`
+
+==== Date Examples
+
+===== Date Example 1 (DE1)
+
+Given a transaction, find me all other transactions within 7 days prior
+
+`g.V('transaction1').values('date').dateAdd(DT.Days,
-7).as('purchase_date').V().hasLabel('transaction').where(gt('purchase_date')).by('date').by()`
+
+===== Date Example 2 (DE2)
+
+Given two transactions, find me the difference in the dates
+
+`g.V('transaction1').values('date').dateDiff(DT.Days,
V('transaction2').values('date').asDate())`
+
+===== Date Example 3 (DE3)
+
+Given a static value, return me the value as a date
+
+`g.inject('1900-01-01').asDate()`
+
+===== Date Example 4 (DE4)
+
+Find the difference between a transaction and the first of the year
+
+`g.V('transaction1').values('date').dateDiff(DT.Days,
inject(datetime('2023-01-01'))`
+
+
+
+== String Manipulation functions in TinkerPop [[string-function-syntax]]
+
+One of the common gaps that user's find when using Gremlin is that there
+is a lack of string manipulation capabilities within the language
+itself. This requires that users use closures to handle many common
+string manipulation options that users want to do on data in the graph.
+This is a problem for many users as many of the providers prevent the
+use of arbitrary closures due to the security risks so for these users
+there is no way to manipulate strings directly.
+
+=== Proposal
+
+The proposal here is to add a set of steps to handle common string
+manipulation requests from users, the details for each are discussed
+below:
+
+* <<asString, asString()>>
+* <<concat, concat()>>
+* <<length, length()>>
+* <<split, split()>>
+* <<substring, substring()>>
+* <<rTrim, rTrim()>>
+* <<lTrim, lTrim()>>
+* <<trim, trim()>>
+* <<replace, replace()>>
+* <<reverse, reverse()>>
+* <<toUpper, toUpper()>>
+* <<toLower, toLower()>>
+
+=== Gremlin Language Variant Function Names
+
+[cols=",,,,,",options="header",]
+|===
+|Groovy |Java |Python |JavaScript |.NET |Go
+|asString() |asString() |as_string() |asString() |AsString() |AsString()
+
+|concat() |concat() |concat() |concat() |Concat() |Concat()
+
+|length() |length() |length() |length() |Length() |Length()
+
+|split() |split() |split() |split() |Split() |Split()
+
+|substring() |substring() |substring() |substring() |Substring()
+|Substring()
+
+|rTrim() |rTrim() |rtrim() |rTrim() |RTrim() |RTrim()
+
+|lTrim() |lTrim() |ltrim() |lTrim() |LTrim() |LTrim()
+
+|trim() |trim() |trim() |trim() |Trim() |Trim()
+
+|replace() |replace() |replace() |replace() |Replace() |Replace()
+
+|reverse() |reverse() |reverse() |reverse() |Reverse() |Reverse()
+
+|toUpper() |toUpper() |to_upper() |toUpper() |ToUpper() |ToUpper()
+
+|toLower() |toLower() |to_lower() |toLower() |ToLower() |ToLower()
+|===
+
+'''''
+
+== Function Definitions
+
+=== `asString()` [[asString]]
+
+Returns the value of the incoming traverser as a string
+
+==== Signature(s)
+
+`asString()`
+
+`asString(Scope)`
+
+==== Parameters
+
+* Scope - Scope Enum
+
+==== Allowed incoming traverser types
+
+Any data type allowed by TinkerPop
+
+==== Expected Output
+
+A String value representing the string value of the traverser being
+passed in as shown below:
+
+[cols=",,",options="header",]
+|===
+|Incoming Datatype |Example Query |Example Output
+|Integer |`g.inject(29).asString()` |29
+
+|Float |`g.inject(29.0).asString()` |29.0
+
+|String |`g.inject('foo').asString()` |foo
+
+|UUID |`g.inject(UUID.randomUUID()).asString()`
+|47557eed-04e7-4aa4-89eb-9689d26fe94a
+
+|Map
+|`g.inject([["id": 1], ["id": 2, "something":"anything"]]).asString()`
+|[[id:1], [id:2, something:anything]]
+
+|Date |`g.inject(datetime()).asString()` |Sun Nov 04 00:00:00 UTC 2018
+
+|List |`g.inject([1,2,3]).asString()` |[1, 2, 3]
+
+|List (Local Scope) |`g.inject([1,2,3]).asString(local)` |["1", "2",
+"3"]
+
+|Vertex |`g.V(1).asString()` |v[1]
+
+|Edge |`g.E(7).asString()` |e[7][1-knows->2]
+
+|Property |`g.V(1).properties('age').asString()` |vp[age->29]
+
+|null |`g.V().group().by('foo').select(keys).asString()` |null
+|===
+
+'''''
+
+=== `concat()` [[concat]]
+
+Concatenates one or more strings together
+
+==== Signature(s)
+
+`concat(String...)`
+
+`concat(Traversal)`
+
+`concat(Scope, String...)`
+
+`concat(Scope, Traversal)`
+
+==== Parameters
+
+* String... - One or more String values to concatenate to the input
+string
+* Traversal - A traversal value to concatenate
+* Scope - Scope Enum
+
+==== Allowed incoming traverser types
+
+String data types or array, if local scope is used. If a non-string
+traverser, or the list containing non-string values, is passed in then
+an `IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A String value representing the concatenation of all the
+
+....
+g.inject('this').concat('is', 'a', 'test')
+==>thisisatest
+g.V(1).values('first_name').concat(' ').concat(V(1).values('last_name')
+==>John Doe
+g.inject(['this', 'is', 'a', 'test']).concat(local)
+==>thisisatest
+g.inject(['John', ' ']).concat(local, V(1).values('last_name')
+==>John Doe
+....
+
+*Note* `concat()` may also be extended to handle concatenating list
+values together but that is out of scope for this change.
+
+'''''
+
+=== `length()` [[length]]
+
+Returns the length of the input string
+
+==== Signature(s)
+
+`length()`
+
+`length(Scope)`
+
+==== Parameters
+
+* Scope - Scope Enum
+
+==== Allowed incoming traverser types
+
+String data types or array, if local scope is used. If a non-string
+traverser, or the list containing non-string values, is passed in then
+an `IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A Long value representing the number of items in an array or the number
+of characters in a string
+
+....
+g.inject('this').length()
+==>4
+g.inject('this').length(local)
+==>4
+....
+
+*Note*:While this is similar to `count(local)` they are not the same.
+`count(local)` treats the input by calculating the count of the items
+stored within the traversal. `length()` treats the input as an array and
+provides the length of that array.
+
+[cols=",,,",options="header",]
+|===
+|Input Datatype |Example traversal |count(local) |length()
+|Integer |`g.inject(29)` |1 |IllegalArgumentException
+
+|Float |`g.inject(29.0)` |1 |IllegalArgumentException
+
+|String |`g.inject('foo')` |1 |3
+
+|UUID |`g.inject(UUID.randomUUID())` |1 |IllegalArgumentException
+
+|Map |`g.inject(["id": 2, "something":"anything"]])` |1
+|IllegalArgumentException
+
+|Date |`g.inject(datetime())` |1 |IllegalArgumentException
+
+|List |`g.inject([1,2,3])` |3 |3
+
+|Vertex |`g.V(1)` |1 |IllegalArgumentException
+
+|Edge |`g.E(7)` |1 |IllegalArgumentException
+
+|Property |`g.V(1).properties('age')` |1 |IllegalArgumentException
+
+|null |`g.V().group().by('foo').select(keys)` |0
+|IllegalArgumentException
+|===
+
+'''''
+
+=== `split()` [[split]]
+
+Returns a list of strings created by splitting the input string around
+the matches of the given delimiter.
+
+==== Signature(s)
+
+`split(String)`
+
+`split(Scope, String)`
+
+==== Parameters
+
+* String - The delimiter character(s) to split the input string* *
+
+==== Allowed inputs
+
+String data types or array, if local scope is used. If a non-string
+traverser, or the list containing non-string values, is passed in then
+an `IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+An array of strings split around the delimiter character(s)
+
+....
+g.inject('this').split('h')
+==>[t, is]
+g.inject('one,two').split(',')
+==>[one, two]
+g.inject('axxb').split('x')
+==>[a, b]
+g.inject('axybxc').split('xy')
+==>[a, bxc]
+g.inject(['this', 'that']).split('h')
+==>[[t, is], [t, at]]
+....
+
+'''''
+
+=== `substring()` [[substring]]
+
+returns a substring of the original string with the length specified,
+uses a 0-based start
+
+==== Signature(s)
+
+`substring(Long, Long)`
+
+`substring(Long)`
+
+`substring(Scope, Long, Long)`
+
+`substring(Scope, Long)`
+
+==== Parameters
+
+* Long - The start index, 0 based. If the value is negative then the
+start location will be the end of the string and it will go the
+specified number of characters from the end of the string.
+* Long - The number of characters to return. Optional - if not provided
+then all remaining characters will be returned
+* Scope - Scope Enum
+
+==== Allowed incoming traverser types
+
+String data types or array, if local scope is used. If a non-string
+traverser, or the list containing non-string values, is passed in then
+an `IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A String value containing the number of characters specified beginning
+at the start location. If the start location plus the length specified
+is greater than or equal to the input length, the result will contain
+the entire string.
+
+....
+g.inject('this').substring(0, 1)
+==>t
+g.inject('this').substring(2)
+==>is
+g.inject('this').substring(2, 5)
+==>is
+g.inject('this').substring(-1)
+==>s
+g.inject(['this', 'is', 'a', 'test']).substring(local, 2)
+==>[is, '' ,'' , 'st']
+....
+
+'''''
+
+=== `rTrim()` [[rTrim]]
+
+Returns a string with trailing whitespace removed
+
+*Note*: Whitespace characters are defined as space/tab/line feed/line
+tabulation/form feed/carriage return.
+
+==== Signature(s)
+
+`rTrim()`
+
+`rTrim(Scope)`
+
+==== Parameters
+
+* Scope - Scope Enum
+
+==== Allowed incoming traverser types
+
+String data types or array, if local scope is used. If a non-string
+traverser, or the list containing non-string values, is passed in then
+an `IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A string value with trailing whitespace removed
+
+....
+g.inject('this ').rTrim()
+==>this
+g.inject(['this ', 'that ']).rTrim(local)
+==>[this, that]
+....
+
+'''''
+
+=== `lTrim()` [[lTrim]]
+
+Returns a string with leading whitespace removed
+
+*Note*: Whitespace characters are defined as space/tab/line feed/line
+tabulation/form feed/carriage return.
+
+==== Signature(s)
+
+`lTrim()`
+
+`lTrim(Scope)`
+
+==== Parameters
+
+* Scope - Scope Enum
+
+==== Allowed incoming traverser types
+
+String data types or array, if local scope is used. If a non-string
+traverser, or the list containing non-string values, is passed in then
+an `IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A string value with leading whitespace removed
+
+....
+g.inject(' this').lTrim()
+==>this
+g.inject([' this', ' that']).lTrim(local)
+==>[this, that]
+....
+
+'''''
+
+=== `trim()` [[trim]]
+
+Returns a string with leading and trailing whitespace removed
+
+*Note*: Whitespace characters are defined as space/tab/line feed/line
+tabulation/form feed/carriage return.
+
+==== Signature(s)
+
+`trim()`
+
+`trim(Scope)`
+
+==== Parameters
+
+* Scope - Scope Enum
+
+==== Allowed incoming traverser types
+
+String data types or array, if local scope is used. If a non-string
+traverser, or the list containing non-string values, is passed in then
+an `IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A string value with leading and trailing whitespace removed
+
+....
+g.inject(' this ').trim()
+==>this
+g.inject([' this ', ' that ']).trim()
+==>[this, that]
+....
+
+'''''
+
+=== `replace()` [[replace]]
+
+Returns a string with the specified characters in the original string
+replaced with the new characters
+
+==== Signature(s)
+
+`replace(String, String)`
+
+`replace(Scope, String, String)`
+
+==== Parameters
+
+* String - The character(s) to be replaced
+* String - The character(s) to replace with
+* Scope - Scope Enum
+
+==== Allowed incoming traverser types
+
+String data types or array, if local scope is used. If a non-string
+traverser, or the list containing non-string values, is passed in then
+an `IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A string
+
+....
+g.inject('this').replace('t', 'x)
+==>xhis
+g.inject('this').replace('x', 't')
+==>this
+g.inject('this').replace('is', 'was')
+==>thwas
+g.inject(['this', 'that']).replace('th', 'was')
+==>[wasis, wasat]
+....
+
+'''''
+
+=== `reverse()` [[reverse]]
+
+Reverses the current string
+
+==== Signature(s)
+
+`reverse()`
+
+`reverse(Scope)`
+
+==== Parameters
+
+* Scope - Scope Enum
+
+==== Allowed incoming traverser types
+
+String data types or array, if local scope is used. If a non-string
+traverser, or the list containing non-string values, is passed in then
+an `IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A String value representing the reversed version of the incoming string
+
+....
+g.inject('this').reverse()
+==>siht
+g.inject(['this', 'that']).reverse(local)
+==>[siht, taht]
+....
+
+*Note* `reverse()` may also be extended to handle concatenating list
+values together but that is out of scope for this change.
+
+'''''
+
+=== `toUpper()` [[toUpper]]
+
+Returns an upper case string representation.
+
+*Note*: All case conversions will be done via the mappings specified for
+Unicode (https://www.unicode.org/reports/tr44/#Casemapping[found here])
+
+==== Signature(s)
+
+`toUpper()`
+
+`toUpper(Scope)`
+
+==== Parameters
+
+* Scope - Scope Enum
+
+==== Allowed incoming traverser types
+
+String data types or array, if local scope is used. If a non-string
+traverser, or the list containing non-string values, is passed in then
+an `IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A string
+
+....
+g.inject('this').toUpper()
+==>THIS
+g.inject(['this', 'that']).toUpper()
+==>[THIS, THAT]
+....
+
+'''''
+
+=== `toLower()` [[toLower]]
+
+Returns an lower case string representation
+
+*Note*: All case conversions will be done via the mappings specified for
+Unicode (https://www.unicode.org/reports/tr44/#Casemapping[found here])
+
+==== Signature(s)
+
+`toLower()`
+
+`toLower(Scope)`
+
+==== Parameters
+
+* Scope - Scope Enum
+
+==== Allowed incoming traverser types
+
+String data types or array, if local scope is used. If a non-string
+traverser, or the list containing non-string values, is passed in then
+an `IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A string
+
+....
+g.inject('THIS').toLower()
+==>this
+g.inject(['THIS', 'THAT']).toLower()
+==>[this, that]
+....
+
+
+== List Manipulation functions in TinkerPop [[list-function-syntax]]
+
+One of the common gaps that user's find when using Gremlin is that there
+is a lack of list manipulation capabilities within the language itself.
+This requires that users use closures to handle many common manipulation
+options that users want to do on data in the graph. This is a problem
+for many users as many of the providers prevent the use of arbitrary
+closures due to the security risks so for these users there is no way to
+manipulate strings directly.
+
+=== Proposal
+
+The proposal here is to add a set of steps to handle common list
+manipulation requests from users, the details for each are discussed
+below:
+
+* <<length_list, length()>>
+* <<reverse_list, reverse()>>
+* <<remove_list, remove()>>
+* <<indexOf_list, indexOf()>>
+* <<product_list, product()>>
+* <<all_list, all()>>
+* <<any_list, any()>>
+* <<none_list, none()>>
+* <<concat_list, concat()>>
+* <<intersect_list, intersect()>>
+* <<union_list, union()>>
+* <<difference_list, difference()>>
+* <<disjunct_list, disjunct()>>
+
+=== Gremlin Language Variant Function Names
+
+[cols=",,,,,",options="header",]
+|===
+|Groovy |Java |Python |JavaScript |.NET |Go
+|length() |length() |length() |length() |Length() |Length()
+
+|reverse() |reverse() |reverse() |reverse() |Reverse() |Reverse()
+
+|remove() |remove() |remove() |remove() |Remove() |Remove()
+
+|indexOf() |indexOf() |index_of() |indexOf() |IndexOf() |IndexOf()
+
+|product() |product() |product() |product() |Product() |Product()
+
+|all() |all() |all() |all() |All() |All()
+
+|any() |any() |any() |any() |Any() |Any()
+
+|none() |none() |none() |none() |None() |None()
+
+|concat() |concat() |concat() |concat() |Concat() |Concat()
+
+|intersect() |intersect() |intersect() |intersect() |Intersect()
+|Intersect()
+
+|union() |union() |union() |union() |Union() |Union()
+
+|difference() |difference() |difference() |difference() |Difference()
+|Difference()
+
+|disjunct() |disjunct() |disjunct() |disjunct() |Disjunct()
+|Disjunct()
+|===
+
+'''''
+
+== Function Definitions
+
+=== `length()` [[length_list]]
+
+Returns the length of a list in the incoming traverser
+
+==== Signature(s)
+
+`length()`
+
+==== Parameters
+
+None
+
+==== Allowed incoming traverser types
+
+Array data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A Long value representing the number of items in an array or the number
+of characters in a string
+
+....
+g.inject([1, 2]).length()
+==>2
+....
+
+=== `reverse()` [[reverse_list]]
+
+Returns the value of the incoming list in reverse order
+
+==== Signature(s)
+
+`reverse()`
+
+==== Parameters
+
+None
+
+==== Allowed incoming traverser types
+
+Array data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+An array in reverse order.
+
+....
+g.inject([1,2]).reverse()
+==>[2, 1]
+....
+
+=== `remove()` [[remove_list]]
+
+Removes the first element from the incoming list where the value equals
+the specified value
+
+==== Signature(s)
+
+`remove(value)`
+
+`remove(Traversal)`
+
+==== Parameters
+
+* value - The value to remove
+
+==== Allowed incoming traverser types
+
+Array data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+An array value representing the new list
+
+....
+g.inject([1,2]).remove(1)
+==>[2]
+....
+
+=== `indexOf()` [[indexOf_list]]
+
+Returns the first occurrence of the `value` in the incoming array
+
+==== Signature(s)
+
+`indexOf(value)`
+
+`indexOf(Traversal)`
+
+==== Parameters
+
+* value - The value to locate
+
+==== Allowed incoming traverser types
+
+Array data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A long representing the index of the first occurrence of the value
+(zero-based). If the values does not exist then `null` is returned
+
+....
+g.inject([1,2]).indexOf(1)
+==>0
+....
+
+=== `product()` [[product_list]]
+
+Returns the cartesian product of two lists
+
+==== Signature(s)
+
+`product(value)`
+
+`product(Traversal)`
+
+==== Parameters
+
+* value - An array
+
+==== Allowed incoming traverser types
+
+Array data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A set of values where each value contains the cartesian product of two
+lists
+
+....
+g.inject([1,2]).product([3,4])
+==>[[1,3], [1,4], [2,3], [2,4]]
+....
+
+=== `any()` [[any_list]]
+
+Returns true if any items in the array `value` exist in the input
+
+==== Signature(s)
+
+`any(value)`
+
+`any(Traversal)`
+
+==== Parameters
+
+* value - An array of the items to check in the incoming list
+
+==== Allowed incoming traverser types
+
+Array data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+True if any values from one list are in the other, False otherwise
+
+....
+g.inject([1,2]).any([1])
+==>true
+g.inject([1,2]).any([3])
+==>false
+....
+
+=== `all()` [[all_list]]
+
+Returns true if all items in the array `value` exist in the input
+
+==== Signature(s)
+
+`all(value)`
+
+`all(Traversal)`
+
+==== Parameters
+
+* value - An array of the items to check in the incoming list
+
+==== Allowed incoming traverser types
+
+Array data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+True if all values from one list are in the other, False otherwise
+
+....
+g.inject([1,2]).all([1])
+==>true
+g.inject([1,2]).all([1, 3])
+==>false
+g.inject([1,2]).all([3])
+==>false
+....
+
+=== `none()` [[none_list]]
+
+Returns true if no items in the array `value` exist in the input
+
+==== Signature(s)
+
+`none(value)`
+
+`none(Traversal)`
+
+==== Parameters
+
+* value - An array of the items to check in the incoming list
+
+==== Allowed incoming traverser types
+
+Array data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+True if no values from one list are in the other, False otherwise
+
+....
+g.inject([1,2]).none([1])
+==>false
+g.inject([1,2]).none([1, 3])
+==>false
+g.inject([1,2]).none([3])
+==>true
+....
+
+=== `concat()` [[concat_list]]
+
+Returns the concatenation of the incoming array and the traversal or
+array value passed as a parameter. This will return all values,
+including duplicates.
+
+==== Signature(s)
+
+`concat(value)`
+
+`concat(Traversal)`
+
+==== Parameters
+
+* value - An array of the items to check in the incoming list
+
+==== Allowed incoming traverser types
+
+Array data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+An array containing the values of the concatenation of the two lists
+
+....
+g.inject([1,2]).concat([3])
+==>[1, 2, 3]
+g.inject([1,2]).concat([1, 4])
+==>[1, 2, 1, 4]
+g.V().has('age', 29).values('age').dedup().fold().concat(V().has('age',
30).values('age').dedup().fold())
+==>[29, 30]
+....
+
+=== `union()` [[union_list]]
+
+Returns the union of the incoming array and the traversal or array value
+passed as a parameter. This will return an array of unique values
+
+==== Signature(s)
+
+`union(value)`
+
+`union(Traversal)`
+
+==== Parameters
+
+* value - An array of the items to check in the incoming list
+
+==== Allowed incoming traverser types
+
+Array data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+An array containing the unique values of the union of the two lists
+
+....
+g.inject([1,2]).union([1])
+==>[1, 2]
+g.inject([1,2]).union([1, 4])
+==>[1, 2, 4]
+g.V().has('age', 29).values('age').dedup().fold().union(V().has('age',
30).values('age').dedup().fold())
+==>[29, 30]
+....
+
+=== `intersect()` [[intersect_list]]
+
+Returns the intersection of the incoming array and the traversal or
+array value passed as a parameter. This will return an array of unique
+values
+
+==== Signature(s)
+
+`intersect(value)`
+
+`intersect(Traversal)`
+
+==== Parameters
+
+* value - An array of the items to check in the incoming list
+
+==== Allowed incoming traverser types
+
+Array data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+An array containing the unique values of the intersection of the two
+lists
+
+....
+g.inject([1,2]).intersect([1])
+==>[1]
+g.inject([1,2]).intersect([1, 2, 3])
+==>[1, 2]
+g.V().has('age', 29).values('age').dedup().fold().intersect(V().has('age',
30).values('age').dedup().fold())
+==>[]
+....
+
+=== `difference()` [[difference_list]]
+
+Returns the difference of the incoming array and the traversal or array
+value passed as a parameter. This will return an array of unique values
+
+==== Signature(s)
+
+`difference(value)`
+
+`difference(Traversal)`
+
+==== Parameters
+
+* value - An array of the items to check in the incoming list
+
+==== Allowed incoming traverser types
+
+Array data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+An array containing the different values of the intersection of the two
+lists
+
+....
+g.inject([1,2]).difference([1])
+==>[2]
+g.inject([1,2]).difference([1, 2, 3])
+==>[3]
+g.V().has('age', 29).values('age').dedup().fold().difference(V().has('age',
30).values('age').dedup().fold())
+==>[29, 30]
+....
+
+....
+
+=== `disjunct()` [[disjunct_list]]
+
+Returns the disjunct set of the incoming array and the traversal or array
+value passed as a parameter. This will return an array of unique values
+
+==== Signature(s)
+
+`disjunct(value)`
+
+`disjunct(Traversal)`
+
+==== Parameters
+
+* value - An array of the items to check in the incoming list
+
+==== Allowed incoming traverser types
+
+Array data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+An array containing the different values of the intersection of the two
+lists
+
+....
+g.inject([1,2]).disjunct([1])
+==>[2]
+g.inject([1,2,4]).disjunct([1, 2, 3])
+==>[3, 4]
+
+
+
+== Date Manipulation functions in TinkerPop [[date-function-syntax]]
+
+One of the common gaps that user's find when using Gremlin is that there
+is a lack of date manipulation capabilities within the language itself.
+This requires that users use closures to handle many common manipulation
+options that users want to do on data in the graph. This is a problem
+for many users as many of the providers prevent the use of arbitrary
+closures due to the security risks so for these users there is no way to
+manipulate strings directly.
+
+=== Proposal
+
+The proposal here is to add a set of steps to handle common datetime
+manipulation requests from users, the details for each are discussed
+below:
+
+* <<asDate, asDate()>>
+* <<dateAdd, dateAdd()>>
+* <<dateDiff, dateDiff()>>
+
+=== Gremlin Language Variant Function Names
+
+[cols=",,,,,",options="header",]
+|===
+|Groovy |Java |Python |JavaScript |.NET |Go
+|asDate() |asDate() |as_date() |asDate() |AsDate() |AsDate()
+|dateAdd() |dateAdd() |date_add() |dateAdd() |DateAdd() |DateAdd()
+|dateDiff() |dateDiff() |date_diff() |dateDiff() |DateDiff() |DateDiff()
+|===
+
+== Function Definitions
+
+=== `asDate()` [[asDate]]
+
+Returns the value of the incoming traverser as an ISO-8601 date
+
+==== Signature(s)
+
+`asDate()`
+
+`asDate(Scope)`
+
+==== Parameters
+
+* Scope - Scope Enum
+
+==== Allowed incoming traverser types
+
+Any data type that can be parsed into an ISO-8601 date. If an
+unsupported types is passed in then an `IllegalArgumentException` will
+be thrown
+
+==== Expected Output
+
+A Date value representing the ISO-8601 value of the traverser being
+passed in as shown below:
+
+[cols=",,",options="header",]
+|===
+|Incoming Datatype |Example Query |Example Output
+|Integer |`g.inject(0).asDate()` |1900-01-01T00:00:00Z
+
+|Float |`g.inject(29.0).asDate()` |1900-01-01T00:00:00Z
+
+|String |`g.inject('1/1/1900').asDate()` |1900-01-01T00:00:00Z
+
+|UUID |`g.inject(UUID.randomUUID()).asDate()`
+|`IllegalArgumentException`
+
+|Map
+|`g.inject([["id": 1], ["id": 2, "something":"anything"]]).asDate()`
+|`IllegalArgumentException`]
+
+|Datetime |`g.inject(datetime()).asDate()` |Sun Nov 04 00:00:00 UTC 2018
+
+|List |`g.inject([1,2,3]).asDate()` |`IllegalArgumentException`
+
+|List (Local Scope) |`g.inject([1,2,3]).asDate(local)`
+|`IllegalArgumentException`
+
+|Vertex |`g.V(1).asDate()` |`IllegalArgumentException`
+
+|Edge |`g.E(7).asDate()` |`IllegalArgumentException`
+
+|Property |`g.V(1).properties('age').asDate()`
+|`IllegalArgumentException`
+
+|null |`g.V().group().by('foo').select(keys).asDate()`
+|`IllegalArgumentException`
+|===
+
+=== `dateAdd()` [[dateAdd]]
+
+Returns the value with the addition of the `value` number of units as
+specified by the `DateToken`
+
+==== Signature(s)
+
+`dateAdd(DateToken, value)`
+
+`dateAdd(Scope, DateToken, value))`
+
+==== Parameters
+
+* DateToken - DateToken Enum
+* value - The number of units, specified by the Datetime Token, to add to
+the incoming values
+
+==== Allowed incoming traverser types
+
+Datetime data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A Datetime with the value added.
+
+....
+g.inject(datetime()).dateAdd(DT.days, 7)
+==> 2018-03-22
+g.inject(datetime()).dateAdd(DT.days, -7)
+==> 2018-03-8
+g.inject([datetime(), datetime()]).dateAdd(local, DT.days, 7)
+==> [2018-03-22, 2018-03-22]
+....
+
+=== `dateDiff()` [[dateDiff]]
+
+Returns the difference between two Datetimes in epoch time
+
+==== Signature(s)
+
+`dateDiff(value)`
+
+`dateDiff(Traversal)`
+
+`dateDiff(Scope, value))`
+
+==== Parameters
+
+* value - The Datetime to find the difference from
+
+==== Allowed incoming traverser types
+
+Datetime data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+The epoch time difference between the two values
+
+....
+g.inject(datetime()).dateDiff(datetime().dateAdd(DT.days, 7))
+==> 604800
+g.inject(datetime()).dateDiff(datetime().dateAdd(DT.days, 7))
+==> -604800
+g.inject([datetime(), datetime()]).dateDiff(local, DT.days, 7)
+==> [604800, 604800]
+....