SimpleTupleFormat.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.geometry.core.internal;

  18. import java.text.ParsePosition;

  19. /** Class for performing simple formatting and parsing of real number tuples.
  20.  */
  21. public class SimpleTupleFormat {

  22.     /** Default value separator string. */
  23.     private static final String DEFAULT_SEPARATOR = ",";

  24.     /** Space character. */
  25.     private static final String SPACE = " ";

  26.     /** Static instance configured with default values. Tuples in this format
  27.      * are enclosed by parentheses and separated by commas.
  28.      */
  29.     private static final SimpleTupleFormat DEFAULT_INSTANCE =
  30.             new SimpleTupleFormat(",", "(", ")");

  31.     /** String separating tuple values. */
  32.     private final String separator;

  33.     /** String used to signal the start of a tuple; may be null. */
  34.     private final String prefix;

  35.     /** String used to signal the end of a tuple; may be null. */
  36.     private final String suffix;

  37.     /** Constructs a new instance with the default string separator (a comma)
  38.      * and the given prefix and suffix.
  39.      * @param prefix String used to signal the start of a tuple; if null, no
  40.      *      string is expected at the start of the tuple
  41.      * @param suffix String used to signal the end of a tuple; if null, no
  42.      *      string is expected at the end of the tuple
  43.      */
  44.     public SimpleTupleFormat(final String prefix, final String suffix) {
  45.         this(DEFAULT_SEPARATOR, prefix, suffix);
  46.     }

  47.     /** Simple constructor.
  48.      * @param separator String used to separate tuple values; must not be null.
  49.      * @param prefix String used to signal the start of a tuple; if null, no
  50.      *      string is expected at the start of the tuple
  51.      * @param suffix String used to signal the end of a tuple; if null, no
  52.      *      string is expected at the end of the tuple
  53.      */
  54.     protected SimpleTupleFormat(final String separator, final String prefix, final String suffix) {
  55.         this.separator = separator;
  56.         this.prefix = prefix;
  57.         this.suffix = suffix;
  58.     }

  59.     /** Return the string used to separate tuple values.
  60.      * @return the value separator string
  61.      */
  62.     public String getSeparator() {
  63.         return separator;
  64.     }

  65.     /** Return the string used to signal the start of a tuple. This value may be null.
  66.      * @return the string used to begin each tuple or null
  67.      */
  68.     public String getPrefix() {
  69.         return prefix;
  70.     }

  71.     /** Returns the string used to signal the end of a tuple. This value may be null.
  72.      * @return the string used to end each tuple or null
  73.      */
  74.     public String getSuffix() {
  75.         return suffix;
  76.     }

  77.     /** Return a tuple string with the given value.
  78.      * @param a value
  79.      * @return 1-tuple string
  80.      */
  81.     public String format(final double a) {
  82.         final StringBuilder sb = new StringBuilder();

  83.         if (prefix != null) {
  84.             sb.append(prefix);
  85.         }

  86.         sb.append(a);

  87.         if (suffix != null) {
  88.             sb.append(suffix);
  89.         }

  90.         return sb.toString();
  91.     }

  92.     /** Return a tuple string with the given values.
  93.      * @param a1 first value
  94.      * @param a2 second value
  95.      * @return 2-tuple string
  96.      */
  97.     public String format(final double a1, final double a2) {
  98.         final StringBuilder sb = new StringBuilder();

  99.         if (prefix != null) {
  100.             sb.append(prefix);
  101.         }

  102.         sb.append(a1)
  103.             .append(separator)
  104.             .append(SPACE)
  105.             .append(a2);

  106.         if (suffix != null) {
  107.             sb.append(suffix);
  108.         }

  109.         return sb.toString();
  110.     }

  111.     /** Return a tuple string with the given values.
  112.      * @param a1 first value
  113.      * @param a2 second value
  114.      * @param a3 third value
  115.      * @return 3-tuple string
  116.      */
  117.     public String format(final double a1, final double a2, final double a3) {
  118.         final StringBuilder sb = new StringBuilder();

  119.         if (prefix != null) {
  120.             sb.append(prefix);
  121.         }

  122.         sb.append(a1)
  123.             .append(separator)
  124.             .append(SPACE)
  125.             .append(a2)
  126.             .append(separator)
  127.             .append(SPACE)
  128.             .append(a3);

  129.         if (suffix != null) {
  130.             sb.append(suffix);
  131.         }

  132.         return sb.toString();
  133.     }

  134.     /** Return a tuple string with the given values.
  135.      * @param a1 first value
  136.      * @param a2 second value
  137.      * @param a3 third value
  138.      * @param a4 fourth value
  139.      * @return 4-tuple string
  140.      */
  141.     public String format(final double a1, final double a2, final double a3, final double a4) {
  142.         final StringBuilder sb = new StringBuilder();

  143.         if (prefix != null) {
  144.             sb.append(prefix);
  145.         }

  146.         sb.append(a1)
  147.             .append(separator)
  148.             .append(SPACE)
  149.             .append(a2)
  150.             .append(separator)
  151.             .append(SPACE)
  152.             .append(a3)
  153.             .append(separator)
  154.             .append(SPACE)
  155.             .append(a4);

  156.         if (suffix != null) {
  157.             sb.append(suffix);
  158.         }

  159.         return sb.toString();
  160.     }

  161.     /** Parse the given string as a 1-tuple and passes the tuple values to the
  162.      * given function. The function output is returned.
  163.      * @param <T> function return type
  164.      * @param str the string to be parsed
  165.      * @param fn function that will be passed the parsed tuple values
  166.      * @return object returned by {@code fn}
  167.      * @throws IllegalArgumentException if the input string format is invalid
  168.      */
  169.     public <T> T parse(final String str, final DoubleFunction1N<T> fn) {
  170.         final ParsePosition pos = new ParsePosition(0);

  171.         readPrefix(str, pos);
  172.         final double v = readTupleValue(str, pos);
  173.         readSuffix(str, pos);
  174.         endParse(str, pos);

  175.         return fn.apply(v);
  176.     }

  177.     /** Parse the given string as a 2-tuple and passes the tuple values to the
  178.      * given function. The function output is returned.
  179.      * @param <T> function return type
  180.      * @param str the string to be parsed
  181.      * @param fn function that will be passed the parsed tuple values
  182.      * @return object returned by {@code fn}
  183.      * @throws IllegalArgumentException if the input string format is invalid
  184.      */
  185.     public <T> T parse(final String str, final DoubleFunction2N<T> fn) {
  186.         final ParsePosition pos = new ParsePosition(0);

  187.         readPrefix(str, pos);
  188.         final double v1 = readTupleValue(str, pos);
  189.         final double v2 = readTupleValue(str, pos);
  190.         readSuffix(str, pos);
  191.         endParse(str, pos);

  192.         return fn.apply(v1, v2);
  193.     }

  194.     /** Parse the given string as a 3-tuple and passes the parsed values to the
  195.      * given function. The function output is returned.
  196.      * @param <T> function return type
  197.      * @param str the string to be parsed
  198.      * @param fn function that will be passed the parsed tuple values
  199.      * @return object returned by {@code fn}
  200.      * @throws IllegalArgumentException if the input string format is invalid
  201.      */
  202.     public <T> T parse(final String str, final DoubleFunction3N<T> fn) {
  203.         final ParsePosition pos = new ParsePosition(0);

  204.         readPrefix(str, pos);
  205.         final double v1 = readTupleValue(str, pos);
  206.         final double v2 = readTupleValue(str, pos);
  207.         final double v3 = readTupleValue(str, pos);
  208.         readSuffix(str, pos);
  209.         endParse(str, pos);

  210.         return fn.apply(v1, v2, v3);
  211.     }

  212.     /** Read the configured prefix from the current position in the given string, ignoring any preceding
  213.      * whitespace, and advance the parsing position past the prefix sequence. An exception is thrown if the
  214.      * prefix is not found. Does nothing if the prefix is null.
  215.      * @param str the string being parsed
  216.      * @param pos the current parsing position
  217.      * @throws IllegalArgumentException if the configured prefix is not null and is not found at the current
  218.      *      parsing position, ignoring preceding whitespace
  219.      */
  220.     private void readPrefix(final String str, final ParsePosition pos) {
  221.         if (prefix != null) {
  222.             consumeWhitespace(str, pos);
  223.             readSequence(str, prefix, pos);
  224.         }
  225.     }

  226.     /** Read and return a tuple value from the current position in the given string. An exception is thrown if a
  227.      * valid number is not found. The parsing position is advanced past the parsed number and any trailing separator.
  228.      * @param str the string being parsed
  229.      * @param pos the current parsing position
  230.      * @return the tuple value
  231.      * @throws IllegalArgumentException if the configured prefix is not null and is not found at the current
  232.      *      parsing position, ignoring preceding whitespace
  233.      */
  234.     private double readTupleValue(final String str, final ParsePosition pos) {
  235.         final int startIdx = pos.getIndex();

  236.         int endIdx = str.indexOf(separator, startIdx);
  237.         if (endIdx < 0) {
  238.             if (suffix != null) {
  239.                 endIdx = str.indexOf(suffix, startIdx);
  240.             }

  241.             if (endIdx < 0) {
  242.                 endIdx = str.length();
  243.             }
  244.         }

  245.         final String substr = str.substring(startIdx, endIdx);
  246.         try {
  247.             final double value = Double.parseDouble(substr);

  248.             // advance the position and move past any terminating separator
  249.             pos.setIndex(endIdx);
  250.             matchSequence(str, separator, pos);

  251.             return value;
  252.         } catch (final NumberFormatException exc) {
  253.             throw parseFailure(String.format("unable to parse number from string \"%s\"", substr), str, pos, exc);
  254.         }
  255.     }

  256.     /** Read the configured suffix from the current position in the given string, ignoring any preceding
  257.      * whitespace, and advance the parsing position past the suffix sequence. An exception is thrown if the
  258.      * suffix is not found. Does nothing if the suffix is null.
  259.      * @param str the string being parsed
  260.      * @param pos the current parsing position
  261.      * @throws IllegalArgumentException if the configured suffix is not null and is not found at the current
  262.      *      parsing position, ignoring preceding whitespace
  263.      */
  264.     private void readSuffix(final String str, final ParsePosition pos) {
  265.         if (suffix != null) {
  266.             consumeWhitespace(str, pos);
  267.             readSequence(str, suffix, pos);
  268.         }
  269.     }

  270.     /** End a parse operation by ensuring that all non-whitespace characters in the string have been parsed. An
  271.      * exception is thrown if extra content is found.
  272.      * @param str the string being parsed
  273.      * @param pos the current parsing position
  274.      * @throws IllegalArgumentException if extra non-whitespace content is found past the current parsing position
  275.      */
  276.     private void endParse(final String str, final ParsePosition pos) {
  277.         consumeWhitespace(str, pos);
  278.         if (pos.getIndex() != str.length()) {
  279.             throw parseFailure("unexpected content", str, pos);
  280.         }
  281.     }

  282.     /** Advance {@code pos} past any whitespace characters in {@code str},
  283.      * starting at the current parse position index.
  284.      * @param str the input string
  285.      * @param pos the current parse position
  286.      */
  287.     private void consumeWhitespace(final String str, final ParsePosition pos) {
  288.         int idx = pos.getIndex();
  289.         final int len = str.length();

  290.         for (; idx < len; ++idx) {
  291.             if (!Character.isWhitespace(str.codePointAt(idx))) {
  292.                 break;
  293.             }
  294.         }

  295.         pos.setIndex(idx);
  296.     }

  297.     /** Return a boolean indicating whether or not the input string {@code str}
  298.      * contains the string {@code seq} at the given parse index. If the match succeeds,
  299.      * the index of {@code pos} is moved to the first character after the match. If
  300.      * the match does not succeed, the parse position is left unchanged.
  301.      * @param str the string to match against
  302.      * @param seq the sequence to look for in {@code str}
  303.      * @param pos the parse position indicating the index in {@code str}
  304.      *      to attempt the match
  305.      * @return true if {@code str} contains exactly the same characters as {@code seq}
  306.      *      at {@code pos}; otherwise, false
  307.      */
  308.     private boolean matchSequence(final String str, final String seq, final ParsePosition pos) {
  309.         final int idx = pos.getIndex();
  310.         final int inputLength = str.length();
  311.         final int seqLength = seq.length();

  312.         int i = idx;
  313.         int s = 0;
  314.         for (; i < inputLength && s < seqLength; ++i, ++s) {
  315.             if (str.codePointAt(i) != seq.codePointAt(s)) {
  316.                 break;
  317.             }
  318.         }

  319.         if (i <= inputLength && s == seqLength) {
  320.             pos.setIndex(idx + seqLength);
  321.             return true;
  322.         }
  323.         return false;
  324.     }

  325.     /** Read the string given by {@code seq} from the given position in {@code str}.
  326.      * Throws an IllegalArgumentException if the sequence is not found at that position.
  327.      * @param str the string to match against
  328.      * @param seq the sequence to look for in {@code str}
  329.      * @param pos the parse position indicating the index in {@code str}
  330.      *      to attempt the match
  331.      * @throws IllegalArgumentException if {@code str} does not contain the characters from
  332.      *      {@code seq} at position {@code pos}
  333.      */
  334.     private void readSequence(final String str, final String seq, final ParsePosition pos) {
  335.         if (!matchSequence(str, seq, pos)) {
  336.             final int idx = pos.getIndex();
  337.             final String actualSeq = str.substring(idx, Math.min(str.length(), idx + seq.length()));

  338.             throw parseFailure(String.format("expected \"%s\" but found \"%s\"", seq, actualSeq), str, pos);
  339.         }
  340.     }

  341.     /** Return an instance configured with default values. Tuples in this format
  342.      * are enclosed by parentheses and separated by commas.
  343.      *
  344.      * Ex:
  345.      * <pre>
  346.      * "(1.0)"
  347.      * "(1.0, 2.0)"
  348.      * "(1.0, 2.0, 3.0)"
  349.      * </pre>
  350.      * @return instance configured with default values
  351.      */
  352.     public static SimpleTupleFormat getDefault() {
  353.         return DEFAULT_INSTANCE;
  354.     }

  355.     /** Return an {@link IllegalArgumentException} representing a parsing failure.
  356.      * @param msg the error message
  357.      * @param str the string being parsed
  358.      * @param pos the current parse position
  359.      * @return an exception signaling a parse failure
  360.      */
  361.     private static IllegalArgumentException parseFailure(final String msg, final String str, final ParsePosition pos) {
  362.         return parseFailure(msg, str, pos, null);
  363.     }

  364.     /** Return an {@link IllegalArgumentException} representing a parsing failure.
  365.      * @param msg the error message
  366.      * @param str the string being parsed
  367.      * @param pos the current parse position
  368.      * @param cause the original cause of the error
  369.      * @return an exception signaling a parse failure
  370.      */
  371.     private static IllegalArgumentException parseFailure(final String msg, final String str, final ParsePosition pos,
  372.                                                          final Throwable cause) {
  373.         final String fullMsg = String.format("Failed to parse string \"%s\" at index %d: %s",
  374.                 str, pos.getIndex(), msg);

  375.         return new TupleParseException(fullMsg, cause);
  376.     }

  377.     /** Exception class for errors occurring during tuple parsing.
  378.      */
  379.     private static class TupleParseException extends IllegalArgumentException {

  380.         /** Serializable version identifier. */
  381.         private static final long serialVersionUID = 20180629;

  382.         /** Simple constructor.
  383.          * @param msg the exception message
  384.          * @param cause the exception root cause
  385.          */
  386.         TupleParseException(final String msg, final Throwable cause) {
  387.             super(msg, cause);
  388.         }
  389.     }
  390. }