| //// |
| 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. |
| //// |
| |
| image::apache-tinkerpop-logo.png[width=500,link="https://tinkerpop.apache.org"] |
| |
| *x.y.z - Proposal 6* |
| |
| == asNumber() Step |
| |
| === Motivation |
| |
| Given the addition of the `asString()` and `asDate()` steps in the 3.7 line, this proposal seeks to bridge another gap in language functionality, which is number casting. |
| |
| === Definition |
| |
| The `asNumber()` step will convert the incoming traverser to the nearest parsable type (e.g. int or double) if no argument is provided, or to the desired numerical type, based on the number token (`N`) provided. Like the `asDate()` step, it will not be scoped (for now, scopes can be added in the future). |
| |
| The proposed tokens, subject to change based on final implementation, are: |
| `N.byte`, `N.short`, `N.int`, `N.long`, `N.float`, `N.double`, `N.bigInt`, `N.bigDecimal` |
| |
| The overloads are: |
| |
| * `asNumber()` |
| * `asNumber(N)` |
| |
| The incoming traverser can be of type: |
| |
| *Number* - the conversion will become a casting operation: |
| [source] |
| ---- |
| gremlin> g.inject(5).asNumber() |
| ==> 5 // parses to int |
| gremlin> g.inject(5.0).asNumber() |
| ==> 5 // parses to double |
| gremlin> g.inject(5.123f).asNumber() |
| ==> 5.123 // will cast float to double |
| ---- |
| |
| * Double to whole number types will be truncated via floor operation |
| |
| [source] |
| ---- |
| gremlin> g.inject(5.43).asNumber(N.int) |
| ==> 5 |
| gremlin> g.inject(5.67).asNumber(N.int) |
| ==> 5 |
| ---- |
| * Widening of types will be a simple cast |
| [source] |
| ---- |
| gremlin> g.inject(5).asNumber(N.long) |
| ==> 5 |
| ---- |
| * Narrowing of types may result in Overflow Exception |
| [source] |
| ---- |
| gremlin> g.inject(12).asNumber(N.byte) |
| ==> 12 |
| gremlin> g.inject(128).asNumber(N.byte) |
| ==> Overflow Exception |
| gremlin> g.inject(300).asNumber(N.byte) |
| ==> Overflow Exception |
| ---- |
| |
| *String* - the conversion will become a parsing operation: |
| |
| * Parsable strings will be parsed to the default type or type specified. Overflow will be treated the same way as if a number was the input. |
| ** Note we can keep things simple for the initial implementation, and throw Parsing Exception for all non-numerical strings, regardless if they are the recognized tokens in Java or Gremlin language. In other words, all strings below will be considered illegal inputs: |
| *** Java/Groovy type - “1.0f”, “1.0d”, “1L” |
| *** Java parsing function limits - “1.0f”, “1.0d” |
| *** Gremlin Lang - “1B”, “1S”, “1L”, “1N”, “1.0D”, “1.0F”, “1.0M” |
| [source] |
| ---- |
| gremlin> g.inject("5").asNumber() |
| ==> 5 |
| gremlin> g.inject("5").asNumber(N.int) |
| ==> 5 |
| gremlin> g.inject("1,000").asNumber(N.int) |
| ==> Parsing Exception |
| gremlin> g.inject("128").asNumber(N.byte) |
| ==> Parsing/Overflow Exception |
| ---- |
| |
| * Semi-parsable strings - do we throw exceptions immediately or try to find our way to the specified token type if possible? [Discussion] point. |
| [source] |
| ---- |
| // Given "1.0" should be parsing into double |
| gremlin> g.inject("1.0").asNumber(N.int) |
| ==> Parsing Exception |
| // asNumber() will parse to double, then user will chain with casting to N.int |
| gremlin> g.inject("1.0").asNumber().asNumber(N.int) |
| ==> 1 |
| gremlin> g.inject("1.0").subString(0,1).asNumber(N.int) |
| ==> 1 |
| |
| Other Options: |
| // Make the step smart to recognize it can be parsed then casted: |
| // 1) parse to double |
| // 2) cast to token type |
| gremlin> g.inject("1.0").asNumber(N.int) |
| ==> 1 |
| |
| // Or cast via substring based on types: |
| // 1) trunct all decimal place of string (make sure string is parsable) |
| // 2) cast to token type |
| gremlin> g.inject("1.0").asNumber(N.int) |
| ==> 1 |
| // Note: this option is favorable because of potential precision loss, eg: |
| // (long) Double.parseDouble("123456789123456789.0") |
| // ==> 123456789123456784 |
| // However, this may be more complex, i.e. what if we get a very long string with letters mixed in the decimal place? |
| ---- |
| * Non-parsable strings - throw exception |
| [source] |
| ---- |
| gremlin> g.inject("test").asNumber() |
| ==> Parsing Exception |
| ---- |
| |
| *Array, List, & Set* - throws exceptions, unless unfolded: |
| |
| * Omit scopes in the first iteration to be consistent with `asDate()`. In this case, user would need to use `unfold()`/`fold()`, or else a Parsing Exception will be thrown. |
| [source] |
| ---- |
| gremlin> g.inject([1, 2, 3, 4]).asNumber() |
| ==> Parsing Exception |
| |
| gremlin> g.inject([1, 2, 3, 4]).unfold().asNumber() |
| ==> 1 |
| ==> 2 |
| ==> 3 |
| ==> 4 |
| |
| gremlin> g.inject([1, 2, 3, 4]).unfold().asNumber().fold() |
| ==> [1, 2, 3, 4] |
| ---- |
| |
| * Scopes can potentially be added in future iterations |
| ** `asNumber(Scope)` |
| ** `asNumber(Scope, N)` |
| *** `Scope.global` - the default scope, will throw an exception since a list cannot be converted to a number |
| *** `Scope.local` - the individual items inside will be evaluated and converted |
| |
| [source] |
| ---- |
| gremlin> g.inject([1, 2, 3, 4]).asNumber() |
| ==> Parsing Exception |
| |
| gremlin> g.inject([1, 2, 3, 4]).asNumber(local) |
| ==> [1, 2, 3, 4] |
| gremlin> g.inject([1, "2", 3, "4.0"]).asNumber(local) |
| ==> [1.0, 2.0, 3.0, 4.0] |
| gremlin> g.inject([1, "two", 3, "4.0"]).asNumber(local) |
| ==> Parsing Exception |
| ---- |
| |
| *Non-Parsable Types* - throws exception |
| [source] |
| ---- |
| gremlin> g.V(1).asNumber(N.int) |
| ==> Parsing Exception ("Type Vertex is not parsable to Type Integer") |
| ---- |
| |
| |