You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by er...@apache.org on 2018/07/06 22:41:49 UTC

[commons-geometry] 02/08: GEOMETRY-3: adding generic coordinate format and parsing classes

This is an automated email from the ASF dual-hosted git repository.

erans pushed a commit to branch GEOMETRY-3__TBR
in repository https://gitbox.apache.org/repos/asf/commons-geometry.git

commit f255ccce7c5736f2eb88c91ef57a4483adede29c
Author: Matt Juntunen <ma...@hotmail.com>
AuthorDate: Sat Jun 2 00:54:34 2018 -0400

    GEOMETRY-3: adding generic coordinate format and parsing classes
---
 .../core/util/AbstractCoordinateParser.java        | 239 +++++++++++
 .../commons/geometry/core/util/Coordinates.java    |  65 +++
 .../geometry/core/util/SimpleCoordinateFormat.java | 177 ++++++++
 .../core/util/SimpleCoordinateFormatTest.java      | 453 +++++++++++++++++++++
 4 files changed, 934 insertions(+)

diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/util/AbstractCoordinateParser.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/util/AbstractCoordinateParser.java
new file mode 100644
index 0000000..ec906bc
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/util/AbstractCoordinateParser.java
@@ -0,0 +1,239 @@
+/*
+ * 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.commons.geometry.core.util;
+
+import java.text.ParsePosition;
+
+/** Abstract class providing basic parsing functionality for reading coordinate tuples
+ * from strings.
+ */
+public abstract class AbstractCoordinateParser {
+
+    /** String separating coordinate values */
+    private final String separator;
+
+    /** String used to signal the start of a coordinate tuple; may be null */
+    private final String prefix;
+
+    /** String used to signal the end of a coordinate tuple; may be null */
+    private final String suffix;
+
+    /** Simple constructor
+     * @param separator String used to separate coordinate values; must not be null.
+     * @param prefix String used to signal the start of a coordinate tuple; if null, no
+     *      string is expected at the start of the tuple
+     * @param suffix String used to signal the end of a coordinate tuple; if null, no
+     *      string is expected at the end of the tuple
+     */
+    protected AbstractCoordinateParser(String separator, String prefix, String suffix) {
+        this.separator = separator;
+        this.prefix = prefix;
+        this.suffix = suffix;
+    }
+
+    /** Returns the string used to separate coordinate values.
+     * @return the coordinate value separator string
+     */
+    public String getSeparator() {
+        return separator;
+    }
+
+    /** Returns the string used to signal the start of a coordinate tuple. This value may be null.
+     * @return the string used to begin each coordinate tuple or null
+     */
+    public String getPrefix() {
+        return prefix;
+    }
+
+    /** Returns the string used to signal the end of a coordinate tuple. This value may be null.
+     * @return the string used to end each coordinate tuple or null
+     */
+    public String getSuffix() {
+        return suffix;
+    }
+
+    /** Reads the configured prefix from the current position in the given string, ignoring any preceding
+     * whitespace, and advances the parsing position past the prefix sequence. An exception is thrown if the
+     * prefix is not found. Does nothing if the prefix is null.
+     * @param str the string being parsed
+     * @param pos the current parsing position
+     * @throws IllegalArgumentException if the configured prefix is not null and is not found at the current
+     *      parsing position, ignoring preceding whitespace
+     */
+    protected void readPrefix(String str, ParsePosition pos) throws IllegalArgumentException {
+        if (prefix != null) {
+            consumeWhitespace(str, pos);
+            readSequence(str, prefix, pos);
+        }
+    }
+
+    /** Reads and returns a coordinate value from the current position in the given string. An exception is thrown if a
+     * valid number is not found. The parsing position is advanced past the parsed number and any trailing separator.
+     * @param str the string being parsed
+     * @param pos the current parsing position
+     * @return the coordinate value
+     * @throws IllegalArgumentException if the configured prefix is not null and is not found at the current
+     *      parsing position, ignoring preceding whitespace
+     */
+    protected double readCoordinateValue(String str, ParsePosition pos) throws IllegalArgumentException {
+        final int startIdx = pos.getIndex();
+
+        int endIdx = str.indexOf(separator, startIdx);
+        if (endIdx < 0) {
+            if (suffix != null) {
+                endIdx = str.indexOf(suffix, startIdx);
+            }
+
+            if (endIdx < 0) {
+                endIdx = str.length();
+            }
+        }
+
+        String substr = str.substring(startIdx, endIdx);
+        try {
+            double value = Double.parseDouble(substr);
+
+            // advance the position and move past any terminating separator
+            pos.setIndex(endIdx);
+            matchSequence(str, separator, pos);
+
+            return value;
+        }
+        catch (NumberFormatException exc) {
+            throw new CoordinateParseException("Failed to parse number from string at index " + startIdx + ": " + substr, exc);
+        }
+    }
+
+    /** Reads the configured suffix from the current position in the given string, ignoring any preceding
+     * whitespace, and advances the parsing position past the suffix sequence. An exception is thrown if the
+     * suffix is not found. Does nothing if the suffix is null.
+     * @param str the string being parsed
+     * @param pos the current parsing position
+     * @throws IllegalArgumentException if the configured suffix is not null and is not found at the current
+     *      parsing position, ignoring preceding whitespace
+     */
+    protected void readSuffix(String str, ParsePosition pos) throws IllegalArgumentException {
+        if (suffix != null) {
+            consumeWhitespace(str, pos);
+            readSequence(str, suffix, pos);
+        }
+    }
+
+    /** Ends a parse operation by ensuring that all non-whitespace characters in the string have been parsed. An exception
+     * is thrown if extra content is found.
+     * @param str the string being parsed
+     * @param pos the current parsing position
+     * @throws IllegalArgumentException if extra non-whitespace content is found past the current parsing position
+     */
+    protected void endParse(String str, ParsePosition pos) throws IllegalArgumentException {
+        consumeWhitespace(str, pos);
+        if (pos.getIndex() != str.length()) {
+            throw new CoordinateParseException("Failed to parse string: unexpected content at index " + pos.getIndex());
+        }
+    }
+
+    /** Advances {@code pos} past any whitespace characters in {@code str},
+     * starting at the current parse position index.
+     * @param str the input string
+     * @param pos the current parse position
+     */
+    protected void consumeWhitespace(String str, ParsePosition pos) {
+        int idx = pos.getIndex();
+        final int len = str.length();
+
+        for (; idx<len; ++idx) {
+            if (!Character.isWhitespace(str.codePointAt(idx))) {
+                break;
+            }
+        }
+
+        pos.setIndex(idx);
+    }
+
+    /** Returns a boolean indicating whether or not the input string {@code str}
+     * contains the string {@code seq} at the given parse index. If the match succeeds,
+     * the index of {@code pos} is moved to the first character after the match. If
+     * the match does not succeed, the parse position is left unchanged.
+     * @param str the string to match against
+     * @param seq the sequence to look for in {@code str}
+     * @param pos the parse position indicating the index in {@code str}
+     *      to attempt the match
+     * @return true if {@code str} contains exactly the same characters as {@code seq}
+     *      at {@code pos}; otherwise, false
+     */
+    protected boolean matchSequence(String str, String seq, ParsePosition pos) {
+        final int idx = pos.getIndex();
+        final int inputLength = str.length();
+        final int seqLength = seq.length();
+
+        int i = idx;
+        int s = 0;
+        for (; i<inputLength && s<seqLength; ++i, ++s) {
+            if (str.codePointAt(i) != seq.codePointAt(s)) {
+                break;
+            }
+        }
+
+        if (i <= inputLength && s == seqLength) {
+            pos.setIndex(idx + seqLength);
+            return true;
+        }
+        return false;
+    }
+
+    /** Reads the string given by {@code seq} from the given position in {@code str}.
+     * Throws an IllegalArgumentException if the sequence is not found at that position.
+     * @param str the string to match against
+     * @param seq the sequence to look for in {@code str}
+     * @param pos the parse position indicating the index in {@code str}
+     *      to attempt the match
+     * @throws IllegalArgumentException if {@code str} does not contain the characters from
+     *      {@code seq} at position {@code pos}
+     */
+    protected void readSequence(String str, String seq, ParsePosition pos) throws IllegalArgumentException {
+        if (!matchSequence(str, seq, pos)) {
+            final int idx = pos.getIndex();
+            final String actualSeq = str.substring(idx, Math.min(str.length(), idx + seq.length()));
+
+            throw new CoordinateParseException("Failed to parse string: expected \"" + seq +
+                    "\" but found \"" + actualSeq + "\" at index " + idx);
+        }
+    }
+
+    /** Exception class for errors occurring during coordinate parsing.
+     */
+    private static class CoordinateParseException extends IllegalArgumentException {
+
+        /** Serializable version identifier */
+        private static final long serialVersionUID = 1494716029613981959L;
+
+        /** Simple constructor.
+         * @param msg the exception message.
+         */
+        public CoordinateParseException(String msg) {
+            super(msg);
+        }
+
+        /** Simple constructor with cause.
+         * @param msg the exception message
+         * @param cause the exception root cause
+         */
+        public CoordinateParseException(String msg, Throwable cause) {
+            super(msg, cause);
+        }
+    }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/util/Coordinates.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/util/Coordinates.java
new file mode 100644
index 0000000..fcafd4f
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/util/Coordinates.java
@@ -0,0 +1,65 @@
+/*
+ * 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.commons.geometry.core.util;
+
+/** Utility class for working with coordinate tuples.
+ */
+public class Coordinates {
+
+    /** Interface for classes that create new instances of a type from a single coordinate value.
+     * @param <T> The type created by this factory.
+     */
+    public static interface Factory1D<T> {
+
+        /** Creates a new instance of type T from the given coordinate value.
+         * @param v the first coordinate value
+         * @return a new instance of type T
+         */
+        T create(double v);
+    }
+
+    /** Interface for classes that create new instances of a type from two coordinate values.
+     * @param <T> The type created by this factory.
+     */
+    public static interface Factory2D<T> {
+
+        /** Creates a new instance of type T from the given coordinate values.
+         * @param v1 the first coordinate value
+         * @param v2 the second coordinate value
+         * @return a new instance of type T
+         */
+        T create(double v1, double v2);
+    }
+
+    /** Interface for classes that create new instances of a type from three coordinate values.
+     * @param <T> The type created by this factory.
+     */
+    public static interface Factory3D<T> {
+
+        /** Creates a new instance of type T from the given coordinate values.
+         * @param v1 the first coordinate value
+         * @param v2 the second coordinate value
+         * @param v3 the third coordinate value
+         * @return a new instance of type T
+         */
+        T create(double v1, double v2, double v3);
+    }
+
+    /** Private constructor. */
+    private Coordinates() {
+    }
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/util/SimpleCoordinateFormat.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/util/SimpleCoordinateFormat.java
new file mode 100644
index 0000000..fd66e45
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/util/SimpleCoordinateFormat.java
@@ -0,0 +1,177 @@
+/*
+ * 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.commons.geometry.core.util;
+
+import java.text.ParsePosition;
+
+/** Class for performing simple formatting and parsing of coordinate tuples in common dimensions.
+ */
+public class SimpleCoordinateFormat extends AbstractCoordinateParser {
+
+    /** Default coordinate separator value */
+    private static final String DEFAULT_SEPARATOR = ",";
+
+    /** Space character */
+    private static final String SPACE = " ";
+
+    /** Creates a new format instance with the default separator value and the given
+     * tuple prefix and suffix.
+     * @param prefix coordinate tuple prefix; may be null
+     * @param suffix coordinate tuple suffix; may be null
+     */
+    public SimpleCoordinateFormat(String prefix, String suffix) {
+        this(DEFAULT_SEPARATOR, prefix, suffix);
+    }
+
+    /** Creates a new format instance with the given separator, prefix, and suffix.
+     * @param separator string separating coordinate values
+     * @param prefix coordinate tuple prefix; may be null
+     * @param suffix coordinate tuple suffix; may be null
+     */
+    public SimpleCoordinateFormat(String separator, String prefix, String suffix) {
+        super(separator, prefix, suffix);
+    }
+
+    /** Returns a 1D coordinate tuple string with the given value.
+     * @param v coordinate value
+     * @return 1D coordinate tuple string
+     */
+    public String format1D(double v) {
+        StringBuilder sb = new StringBuilder();
+
+        if (getPrefix() != null) {
+            sb.append(getPrefix());
+        }
+
+        sb.append(v);
+
+        if (getSuffix() != null) {
+            sb.append(getSuffix());
+        }
+
+        return sb.toString();
+    }
+
+    /** Returns a 2D coordinate tuple string with the given values.
+     * @param v1 first coordinate value
+     * @param v2 second coordinate value
+     * @return 2D coordinate tuple string
+     */
+    public String format2D(double v1, double v2) {
+        StringBuilder sb = new StringBuilder();
+
+        if (getPrefix() != null) {
+            sb.append(getPrefix());
+        }
+
+        sb.append(v1);
+        sb.append(getSeparator());
+        sb.append(SPACE);
+        sb.append(v2);
+
+        if (getSuffix() != null) {
+            sb.append(getSuffix());
+        }
+
+        return sb.toString();
+    }
+
+    /** Returns a 3D coordinate tuple string with the given values.
+     * @param v1 first coordinate value
+     * @param v2 second coordinate value
+     * @param v3 third coordinate value
+     * @return 3D coordinate tuple string
+     */
+    public String format3D(double v1, double v2, double v3) {
+        StringBuilder sb = new StringBuilder();
+
+        if (getPrefix() != null) {
+            sb.append(getPrefix());
+        }
+
+        sb.append(v1);
+        sb.append(getSeparator());
+        sb.append(SPACE);
+        sb.append(v2);
+        sb.append(getSeparator());
+        sb.append(SPACE);
+        sb.append(v3);
+
+        if (getSuffix() != null) {
+            sb.append(getSuffix());
+        }
+
+        return sb.toString();
+    }
+
+    /** Parses the given string as a 1D coordinate tuple and passes the coordinate value to the
+     * given factory. The object created by the factory is returned.
+     * @param str the string to be parsed
+     * @param factory object that will be passed the parsed coordinate value
+     * @return object created by {@code factory}
+     * @throws IllegalArgumentException if the input string format is invalid
+     */
+    public <T> T parse1D(String str, Coordinates.Factory1D<T> factory) throws IllegalArgumentException {
+        final ParsePosition pos = new ParsePosition(0);
+
+        readPrefix(str, pos);
+        final double v = readCoordinateValue(str, pos);
+        readSuffix(str, pos);
+        endParse(str, pos);
+
+        return factory.create(v);
+    }
+
+    /** Parses the given string as a 2D coordinate tuple and passes the coordinate values to the
+     * given factory. The object created by the factory is returned.
+     * @param str the string to be parsed
+     * @param factory object that will be passed the parsed coordinate values
+     * @return object created by {@code factory}
+     * @throws IllegalArgumentException if the input string format is invalid
+     */
+    public <T> T parse2D(String str, Coordinates.Factory2D<T> factory) throws IllegalArgumentException {
+        final ParsePosition pos = new ParsePosition(0);
+
+        readPrefix(str, pos);
+        final double v1 = readCoordinateValue(str, pos);
+        final double v2 = readCoordinateValue(str, pos);
+        readSuffix(str, pos);
+        endParse(str, pos);
+
+        return factory.create(v1, v2);
+    }
+
+    /** Parses the given string as a 3D coordinate tuple and passes the coordinate values to the
+     * given factory. The object created by the factory is returned.
+     * @param str the string to be parsed
+     * @param factory object that will be passed the parsed coordinate values
+     * @return object created by {@code factory}
+     * @throws IllegalArgumentException if the input string format is invalid
+     */
+    public <T> T parse3D(String str, Coordinates.Factory3D<T> factory) throws IllegalArgumentException {
+        ParsePosition pos = new ParsePosition(0);
+
+        readPrefix(str, pos);
+        final double v1 = readCoordinateValue(str, pos);
+        final double v2 = readCoordinateValue(str, pos);
+        final double v3 = readCoordinateValue(str, pos);
+        readSuffix(str, pos);
+        endParse(str, pos);
+
+        return factory.create(v1, v2, v3);
+    }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/util/SimpleCoordinateFormatTest.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/util/SimpleCoordinateFormatTest.java
new file mode 100644
index 0000000..820d3f6
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/util/SimpleCoordinateFormatTest.java
@@ -0,0 +1,453 @@
+/*
+ * 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.commons.geometry.core.util;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class SimpleCoordinateFormatTest {
+
+    private static final double EPS = 1e-10;
+
+    private static final String COMMA = ",";
+    private static final String OPEN_PAREN = "(";
+    private static final String CLOSE_PAREN = ")";
+
+    private static Coordinates.Factory1D<Stub1D> FACTORY_1D = new Coordinates.Factory1D<Stub1D>() {
+
+        @Override
+        public Stub1D create(double v) {
+            Stub1D result = new Stub1D();
+            result.v = v;
+
+            return result;
+        }
+    };
+
+    private static Coordinates.Factory2D<Stub2D> FACTORY_2D = new Coordinates.Factory2D<Stub2D>() {
+
+        @Override
+        public Stub2D create(double v1, double v2) {
+            Stub2D result = new Stub2D();
+            result.v1 = v1;
+            result.v2 = v2;
+
+            return result;
+        }
+    };
+
+    private static Coordinates.Factory3D<Stub3D> FACTORY_3D = new Coordinates.Factory3D<Stub3D>() {
+
+        @Override
+        public Stub3D create(double v1, double v2, double v3) {
+            Stub3D result = new Stub3D();
+            result.v1 = v1;
+            result.v2 = v2;
+            result.v3 = v3;
+
+            return result;
+        }
+    };
+
+    @Test
+    public void testConstructor() {
+        // act
+        SimpleCoordinateFormat formatter = new SimpleCoordinateFormat("|", "{", "}");
+
+        // assert
+        Assert.assertEquals("|", formatter.getSeparator());
+        Assert.assertEquals("{", formatter.getPrefix());
+        Assert.assertEquals("}", formatter.getSuffix());
+    }
+
+    @Test
+    public void testConstructor_defaultSeparator() {
+        // act
+        SimpleCoordinateFormat formatter = new SimpleCoordinateFormat("{", "}");
+
+        // assert
+        Assert.assertEquals(COMMA, formatter.getSeparator());
+        Assert.assertEquals("{", formatter.getPrefix());
+        Assert.assertEquals("}", formatter.getSuffix());
+    }
+
+    @Test
+    public void testFormat1D() {
+        // arrange
+        SimpleCoordinateFormat formatter = new SimpleCoordinateFormat(OPEN_PAREN, CLOSE_PAREN);
+
+        // act/assert
+        Assert.assertEquals("(1.0)", formatter.format1D(1.0));
+        Assert.assertEquals("(-1.0)", formatter.format1D(-1.0));
+        Assert.assertEquals("(NaN)", formatter.format1D(Double.NaN));
+        Assert.assertEquals("(-Infinity)", formatter.format1D(Double.NEGATIVE_INFINITY));
+        Assert.assertEquals("(Infinity)", formatter.format1D(Double.POSITIVE_INFINITY));
+    }
+
+    @Test
+    public void testFormat1D_noPrefixSuffix() {
+        // arrange
+        SimpleCoordinateFormat formatter = new SimpleCoordinateFormat(null, null);
+
+        // act/assert
+        Assert.assertEquals("1.0", formatter.format1D(1.0));
+        Assert.assertEquals("-1.0", formatter.format1D(-1.0));
+        Assert.assertEquals("NaN", formatter.format1D(Double.NaN));
+        Assert.assertEquals("-Infinity", formatter.format1D(Double.NEGATIVE_INFINITY));
+        Assert.assertEquals("Infinity", formatter.format1D(Double.POSITIVE_INFINITY));
+    }
+
+    @Test
+    public void testFormat2D() {
+        // arrange
+        SimpleCoordinateFormat formatter = new SimpleCoordinateFormat(OPEN_PAREN, CLOSE_PAREN);
+
+        // act/assert
+        Assert.assertEquals("(1.0, -1.0)", formatter.format2D(1.0, -1.0));
+        Assert.assertEquals("(-1.0, 1.0)", formatter.format2D(-1.0, 1.0));
+        Assert.assertEquals("(NaN, -Infinity)", formatter.format2D(Double.NaN, Double.NEGATIVE_INFINITY));
+        Assert.assertEquals("(-Infinity, Infinity)", formatter.format2D(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY));
+    }
+
+    @Test
+    public void testFormat2D_noPrefixSuffix() {
+        // arrange
+        SimpleCoordinateFormat formatter = new SimpleCoordinateFormat(null, null);
+
+        // act/assert
+        Assert.assertEquals("1.0, -1.0", formatter.format2D(1.0, -1.0));
+        Assert.assertEquals("-1.0, 1.0", formatter.format2D(-1.0, 1.0));
+        Assert.assertEquals("NaN, -Infinity", formatter.format2D(Double.NaN, Double.NEGATIVE_INFINITY));
+        Assert.assertEquals("-Infinity, Infinity", formatter.format2D(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY));
+    }
+
+    @Test
+    public void testFormat3D() {
+        // arrange
+        SimpleCoordinateFormat formatter = new SimpleCoordinateFormat(OPEN_PAREN, CLOSE_PAREN);
+
+        // act/assert
+        Assert.assertEquals("(1.0, 0.0, -1.0)", formatter.format3D(1.0, 0.0, -1.0));
+        Assert.assertEquals("(-1.0, 1.0, 0.0)", formatter.format3D(-1.0, 1.0, 0.0));
+        Assert.assertEquals("(NaN, -Infinity, Infinity)", formatter.format3D(Double.NaN, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY));
+    }
+
+    @Test
+    public void testFormat3D_noPrefixSuffix() {
+        // arrange
+        SimpleCoordinateFormat formatter = new SimpleCoordinateFormat(null, null);
+
+        // act/assert
+        Assert.assertEquals("1.0, 0.0, -1.0", formatter.format3D(1.0, 0.0, -1.0));
+        Assert.assertEquals("-1.0, 1.0, 0.0", formatter.format3D(-1.0, 1.0, 0.0));
+        Assert.assertEquals("NaN, -Infinity, Infinity", formatter.format3D(Double.NaN, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY));
+    }
+
+    @Test
+    public void testFormat_longTokens() {
+        // arrange
+        SimpleCoordinateFormat formatter = new SimpleCoordinateFormat("||", "<<", ">>");
+
+        // act/assert
+        Assert.assertEquals("<<1.0>>", formatter.format1D(1.0));
+        Assert.assertEquals("<<1.0|| 2.0>>", formatter.format2D(1.0, 2.0));
+        Assert.assertEquals("<<1.0|| 2.0|| 3.0>>", formatter.format3D(1.0, 2.0, 3.0));
+    }
+
+    @Test
+    public void testParse1D() {
+        // arrange
+        SimpleCoordinateFormat formatter = new SimpleCoordinateFormat(OPEN_PAREN, CLOSE_PAREN);
+
+        // act/assert
+        checkParse1D(formatter, "(1)", 1.0);
+        checkParse1D(formatter, "(-1)", -1.0);
+
+        checkParse1D(formatter, "(0.01)", 0.01);
+        checkParse1D(formatter, "(-1e-2)", -0.01);
+
+        checkParse1D(formatter, "(100)", 100);
+        checkParse1D(formatter, "(-1e2)", -100);
+
+        checkParse1D(formatter, " (\n 1 \t) ", 1);
+        checkParse1D(formatter, "\n ( -1 \t)\r\n", -1);
+
+        checkParse1D(formatter, "(1, )", 1.0);
+        checkParse1D(formatter, "(-1, )", -1.0);
+
+        checkParse1D(formatter, "(NaN)", Double.NaN);
+        checkParse1D(formatter, "(-Infinity)", Double.NEGATIVE_INFINITY);
+        checkParse1D(formatter, "(Infinity)", Double.POSITIVE_INFINITY);
+    }
+
+    @Test
+    public void testParse1D_noPrefixSuffix() {
+        // arrange
+        SimpleCoordinateFormat formatter = new SimpleCoordinateFormat(null, null);
+
+        // act/assert
+        checkParse1D(formatter, "1", 1.0);
+        checkParse1D(formatter, "-1", -1.0);
+
+        checkParse1D(formatter, "0.01", 0.01);
+        checkParse1D(formatter, "-1e-2", -0.01);
+
+        checkParse1D(formatter, "100", 100);
+        checkParse1D(formatter, "-1e2", -100);
+
+        checkParse1D(formatter, " \n 1 \t ", 1);
+        checkParse1D(formatter, "\n  -1 \t\r\n", -1);
+
+        checkParse1D(formatter, "1, ", 1.0);
+        checkParse1D(formatter, "-1, ", -1.0);
+
+        checkParse1D(formatter, "NaN", Double.NaN);
+        checkParse1D(formatter, "-Infinity", Double.NEGATIVE_INFINITY);
+        checkParse1D(formatter, "Infinity", Double.POSITIVE_INFINITY);
+    }
+
+    @Test
+    public void testParse1D_failure() {
+        // arrange
+        SimpleCoordinateFormat formatter = new SimpleCoordinateFormat(OPEN_PAREN, CLOSE_PAREN);
+
+        // act/assert
+        checkParse1DFailure(formatter, "", "expected \"(\" but found \"\" at index 0");
+        checkParse1DFailure(formatter, "(1 ", "expected \")\" but found \"\" at index 3");
+
+        checkParse1DFailure(formatter, "(abc)", "Failed to parse number from string at index 1: abc");
+
+        checkParse1DFailure(formatter, "(1) 1", "unexpected content at index 4");
+    }
+
+    @Test
+    public void testParse2D() {
+        // arrange
+        SimpleCoordinateFormat formatter = new SimpleCoordinateFormat(OPEN_PAREN, CLOSE_PAREN);
+
+        // act/assert
+        checkParse2D(formatter, "(1,-2)", 1.0, -2.0);
+        checkParse2D(formatter, "(2,-1)", 2.0, -1.0);
+
+        checkParse2D(formatter, "(0.01, -0.02)", 0.01, -0.02);
+        checkParse2D(formatter, "(-1e-2,2e-2)", -0.01, 0.02);
+
+        checkParse2D(formatter, "(100,  -1e2)", 100, -100);
+
+        checkParse2D(formatter, " (\n 1 , 2 \t) ", 1, 2);
+        checkParse2D(formatter, "\n ( -1 , -2 \t)\r\n", -1, -2);
+
+        checkParse2D(formatter, "(1, 2, )", 1.0, 2.0);
+        checkParse2D(formatter, "(-1, -2,)", -1.0, -2.0);
+
+        checkParse2D(formatter, "(NaN, -Infinity)", Double.NaN, Double.NEGATIVE_INFINITY);
+        checkParse2D(formatter, "(-Infinity, Infinity)", Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
+    }
+
+    @Test
+    public void testParse2D_noPrefixSuffix() {
+        // arrange
+        SimpleCoordinateFormat formatter = new SimpleCoordinateFormat(null, null);
+
+        // act/assert
+        checkParse2D(formatter, "1,-2", 1.0, -2.0);
+        checkParse2D(formatter, "2,-1", 2.0, -1.0);
+
+        checkParse2D(formatter, "0.01, -0.02", 0.01, -0.02);
+        checkParse2D(formatter, "-1e-2,2e-2", -0.01, 0.02);
+
+        checkParse2D(formatter, "100,  -1e2", 100, -100);
+
+        checkParse2D(formatter, " \n 1 , 2 \t ", 1, 2);
+        checkParse2D(formatter, "\n  -1 , -2 \t\r\n", -1, -2);
+
+        checkParse2D(formatter, "1, 2, ", 1.0, 2.0);
+        checkParse2D(formatter, "-1, -2,", -1.0, -2.0);
+
+        checkParse2D(formatter, "NaN, -Infinity", Double.NaN, Double.NEGATIVE_INFINITY);
+        checkParse2D(formatter, "-Infinity, Infinity", Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
+    }
+
+    @Test
+    public void testParse2D_failure() {
+        // arrange
+        SimpleCoordinateFormat formatter = new SimpleCoordinateFormat(OPEN_PAREN, CLOSE_PAREN);
+
+        // act/assert
+        checkParse2DFailure(formatter, "", "expected \"(\" but found \"\" at index 0");
+        checkParse2DFailure(formatter, "(1, 2 ", "expected \")\" but found \"\" at index 6");
+
+        checkParse2DFailure(formatter, "(0,abc)", "Failed to parse number from string at index 3: abc");
+
+        checkParse2DFailure(formatter, "(1, 2) 1", "unexpected content at index 7");
+    }
+
+    @Test
+    public void testParse3D() {
+        // arrange
+        SimpleCoordinateFormat formatter = new SimpleCoordinateFormat(OPEN_PAREN, CLOSE_PAREN);
+
+        // act/assert
+        checkParse3D(formatter, "(1,-2,3)", 1.0, -2.0, 3.0);
+        checkParse3D(formatter, "(2,-1,3)", 2.0, -1.0, 3.0);
+
+        checkParse3D(formatter, "(0.01, -0.02, 0.3)", 0.01, -0.02, 0.3);
+        checkParse3D(formatter, "(-1e-2,2e-2,-3E-1)", -0.01, 0.02, -0.3);
+
+        checkParse3D(formatter, "(100,  -1e2,2E10)", 100, -100, 2e10);
+
+        checkParse3D(formatter, " (\n 1 , 2 , 3 \t) ", 1, 2, 3);
+        checkParse3D(formatter, "\n ( -1 , -2 ,  -3 \t)\r\n", -1, -2, -3);
+
+        checkParse3D(formatter, "(1, 2, 3, )", 1.0, 2.0, 3.0);
+        checkParse3D(formatter, "(-1, -2, -3,)", -1.0, -2.0, -3.0);
+
+        checkParse3D(formatter, "(NaN, -Infinity, Infinity)", Double.NaN, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
+    }
+
+    @Test
+    public void testParse3D_noPrefixSuffix() {
+        // arrange
+        SimpleCoordinateFormat formatter = new SimpleCoordinateFormat(null, null);
+
+        // act/assert
+        checkParse3D(formatter, "1,-2,3", 1.0, -2.0, 3.0);
+        checkParse3D(formatter, "2,-1,3", 2.0, -1.0, 3.0);
+
+        checkParse3D(formatter, "0.01, -0.02, 0.3", 0.01, -0.02, 0.3);
+        checkParse3D(formatter, "-1e-2,2e-2,-3E-1", -0.01, 0.02, -0.3);
+
+        checkParse3D(formatter, "100,  -1e2,2E10", 100, -100, 2e10);
+
+        checkParse3D(formatter, " \n 1 , 2 , 3 \t ", 1, 2, 3);
+        checkParse3D(formatter, "\n  -1 , -2 ,  -3 \t\r\n", -1, -2, -3);
+
+        checkParse3D(formatter, "1, 2, 3, ", 1.0, 2.0, 3.0);
+        checkParse3D(formatter, "-1, -2, -3,", -1.0, -2.0, -3.0);
+
+        checkParse3D(formatter, "NaN, -Infinity, Infinity", Double.NaN, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
+    }
+
+    @Test
+    public void testParse3D_failure() {
+        // arrange
+        SimpleCoordinateFormat formatter = new SimpleCoordinateFormat(OPEN_PAREN, CLOSE_PAREN);
+
+        // act/assert
+        checkParse3DFailure(formatter, "", "expected \"(\" but found \"\" at index 0");
+        checkParse3DFailure(formatter, "(1, 2, 3", "expected \")\" but found \"\" at index 8");
+
+        checkParse3DFailure(formatter, "(0,0,abc)", "Failed to parse number from string at index 5: abc");
+
+        checkParse3DFailure(formatter, "(1, 2, 3) 1", "unexpected content at index 10");
+    }
+
+    @Test
+    public void testParse_longTokens() {
+        // arrange
+        SimpleCoordinateFormat formatter = new SimpleCoordinateFormat("||", "<<", ">>");
+
+        // act/assert
+        checkParse1D(formatter, "<<1.0>>", 1.0);
+        checkParse2D(formatter, "<<1.0|| 2.0>>", 1.0, 2.0);
+        checkParse3D(formatter, "<<1.0|| 2.0|| 3.0>>", 1.0, 2.0, 3.0);
+    }
+
+    @Test
+    public void testParse_longTokens_failure() {
+        // arrange
+        SimpleCoordinateFormat formatter = new SimpleCoordinateFormat("||", "<<", ">>");
+
+        // act/assert
+        checkParse1DFailure(formatter, "<", "expected \"<<\" but found \"<\" at index 0");
+        checkParse1DFailure(formatter, "<1.0>>", "expected \"<<\" but found \"<1\" at index 0");
+        checkParse2DFailure(formatter, "<<1.0| 2.0>>", "Failed to parse number from string at index 2: 1.0| 2.0");
+        checkParse3DFailure(formatter, "<<1.0|| 2.0|| 3.0>", "Failed to parse number from string at index 13:  3.0>");
+    }
+
+    private void checkParse1D(SimpleCoordinateFormat formatter, String str, double v) {
+        Stub1D result = formatter.parse1D(str, FACTORY_1D);
+
+        Assert.assertEquals(v, result.v, EPS);
+    }
+
+    private void checkParse1DFailure(SimpleCoordinateFormat formatter, String str, String msgSubstr) {
+        try {
+            formatter.parse1D(str, FACTORY_1D);
+            Assert.fail("Operation should have failed");
+        }
+        catch (IllegalArgumentException exc) {
+            String excMsg = exc.getMessage();
+            Assert.assertTrue("Expected message to contain [" + msgSubstr + "] but was [" + excMsg + "]",
+                    excMsg.contains(msgSubstr));
+        }
+    }
+
+    private void checkParse2D(SimpleCoordinateFormat formatter, String str, double v1, double v2) {
+        Stub2D result = formatter.parse2D(str, FACTORY_2D);
+
+        Assert.assertEquals(v1, result.v1, EPS);
+        Assert.assertEquals(v2, result.v2, EPS);
+    }
+
+    private void checkParse2DFailure(SimpleCoordinateFormat formatter, String str, String msgSubstr) {
+        try {
+            formatter.parse2D(str, FACTORY_2D);
+            Assert.fail("Operation should have failed");
+        }
+        catch (IllegalArgumentException exc) {
+            String excMsg = exc.getMessage();
+            Assert.assertTrue("Expected message to contain [" + msgSubstr + "] but was [" + excMsg + "]",
+                    excMsg.contains(msgSubstr));
+        }
+    }
+
+    private void checkParse3D(SimpleCoordinateFormat formatter, String str, double v1, double v2, double v3) {
+        Stub3D result = formatter.parse3D(str, FACTORY_3D);
+
+        Assert.assertEquals(v1, result.v1, EPS);
+        Assert.assertEquals(v2, result.v2, EPS);
+        Assert.assertEquals(v3, result.v3, EPS);
+    }
+
+    private void checkParse3DFailure(SimpleCoordinateFormat formatter, String str, String msgSubstr) {
+        try {
+            formatter.parse3D(str, FACTORY_3D);
+            Assert.fail("Operation should have failed");
+        }
+        catch (IllegalArgumentException exc) {
+            String excMsg = exc.getMessage();
+            Assert.assertTrue("Expected message to contain [" + msgSubstr + "] but was [" + excMsg + "]",
+                    excMsg.contains(msgSubstr));
+        }
+    }
+
+    private static class Stub1D {
+        public double v;
+    }
+
+    private static class Stub2D {
+        public double v1;
+        public double v2;
+    }
+
+    private static class Stub3D {
+        public double v1;
+        public double v2;
+        public double v3;
+    }
+}