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 <[email protected]> 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; + } +}
