View Javadoc

    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
    19  import java.text.ParsePosition;
    20
    21  /** Class for performing simple formatting and parsing of real number tuples.
    22   */
    23  public class SimpleTupleFormat {
    24
    25      /** Default value separator string. */
    26      private static final String DEFAULT_SEPARATOR = ",";
    27
    28      /** Space character. */
    29      private static final String SPACE = " ";
    30
    31      /** Static instance configured with default values. Tuples in this format
    32       * are enclosed by parentheses and separated by commas.
    33       */
    34      private static final SimpleTupleFormat DEFAULT_INSTANCE =
    35              new SimpleTupleFormat(",", "(", ")");
    36
    37      /** String separating tuple values. */
    38      private final String separator;
    39
    40      /** String used to signal the start of a tuple; may be null. */
    41      private final String prefix;
    42
    43      /** String used to signal the end of a tuple; may be null. */
    44      private final String suffix;
    45
    46      /** Constructs a new instance with the default string separator (a comma)
    47       * and the given prefix and suffix.
    48       * @param prefix String used to signal the start of a tuple; if null, no
    49       *      string is expected at the start of the tuple
    50       * @param suffix String used to signal the end of a tuple; if null, no
    51       *      string is expected at the end of the tuple
    52       */
    53      public SimpleTupleFormat(final String prefix, final String suffix) {
    54          this(DEFAULT_SEPARATOR, prefix, suffix);
    55      }
    56
    57      /** Simple constructor.
    58       * @param separator String used to separate tuple values; must not be null.
    59       * @param prefix String used to signal the start of a tuple; if null, no
    60       *      string is expected at the start of the tuple
    61       * @param suffix String used to signal the end of a tuple; if null, no
    62       *      string is expected at the end of the tuple
    63       */
    64      protected SimpleTupleFormat(final String separator, final String prefix, final String suffix) {
    65          this.separator = separator;
    66          this.prefix = prefix;
    67          this.suffix = suffix;
    68      }
    69
    70      /** Return the string used to separate tuple values.
    71       * @return the value separator string
    72       */
    73      public String getSeparator() {
    74          return separator;
    75      }
    76
    77      /** Return the string used to signal the start of a tuple. This value may be null.
    78       * @return the string used to begin each tuple or null
    79       */
    80      public String getPrefix() {
    81          return prefix;
    82      }
    83
    84      /** Returns the string used to signal the end of a tuple. This value may be null.
    85       * @return the string used to end each tuple or null
    86       */
    87      public String getSuffix() {
    88          return suffix;
    89      }
    90
    91      /** Return a tuple string with the given value.
    92       * @param a value
    93       * @return 1-tuple string
    94       */
    95      public String format(final double a) {
    96          final StringBuilder sb = new StringBuilder();
    97
    98          if (prefix != null) {
    99              sb.append(prefix);
    100         }
    101
    102         sb.append(a);
    103
    104         if (suffix != null) {
    105             sb.append(suffix);
    106         }
    107
    108         return sb.toString();
    109     }
    110
    111     /** Return a tuple string with the given values.
    112      * @param a1 first value
    113      * @param a2 second value
    114      * @return 2-tuple string
    115      */
    116     public String format(final double a1, final double a2) {
    117         final StringBuilder sb = new StringBuilder();
    118
    119         if (prefix != null) {
    120             sb.append(prefix);
    121         }
    122
    123         sb.append(a1)
    124             .append(separator)
    125             .append(SPACE)
    126             .append(a2);
    127
    128         if (suffix != null) {
    129             sb.append(suffix);
    130         }
    131
    132         return sb.toString();
    133     }
    134
    135     /** Return a tuple string with the given values.
    136      * @param a1 first value
    137      * @param a2 second value
    138      * @param a3 third value
    139      * @return 3-tuple string
    140      */
    141     public String format(final double a1, final double a2, final double a3) {
    142         final StringBuilder sb = new StringBuilder();
    143
    144         if (prefix != null) {
    145             sb.append(prefix);
    146         }
    147
    148         sb.append(a1)
    149             .append(separator)
    150             .append(SPACE)
    151             .append(a2)
    152             .append(separator)
    153             .append(SPACE)
    154             .append(a3);
    155
    156         if (suffix != null) {
    157             sb.append(suffix);
    158         }
    159
    160         return sb.toString();
    161     }
    162
    163     /** Return a tuple string with the given values.
    164      * @param a1 first value
    165      * @param a2 second value
    166      * @param a3 third value
    167      * @param a4 fourth value
    168      * @return 4-tuple string
    169      */
    170     public String format(final double a1, final double a2, final double a3, final double a4) {
    171         final StringBuilder sb = new StringBuilder();
    172
    173         if (prefix != null) {
    174             sb.append(prefix);
    175         }
    176
    177         sb.append(a1)
    178             .append(separator)
    179             .append(SPACE)
    180             .append(a2)
    181             .append(separator)
    182             .append(SPACE)
    183             .append(a3)
    184             .append(separator)
    185             .append(SPACE)
    186             .append(a4);
    187
    188         if (suffix != null) {
    189             sb.append(suffix);
    190         }
    191
    192         return sb.toString();
    193     }
    194
    195     /** Parse the given string as a 1-tuple and passes the tuple values to the
    196      * given function. The function output is returned.
    197      * @param <T> function return type
    198      * @param str the string to be parsed
    199      * @param fn function that will be passed the parsed tuple values
    200      * @return object returned by {@code fn}
    201      * @throws IllegalArgumentException if the input string format is invalid
    202      */
    203     public <T> T parse(final String str, final DoubleFunction1N<T> fn) {
    204         final ParsePosition pos = new ParsePosition(0);
    205
    206         readPrefix(str, pos);
    207         final double v = readTupleValue(str, pos);
    208         readSuffix(str, pos);
    209         endParse(str, pos);
    210
    211         return fn.apply(v);
    212     }
    213
    214     /** Parse the given string as a 2-tuple and passes the tuple values to the
    215      * given function. The function output is returned.
    216      * @param <T> function return type
    217      * @param str the string to be parsed
    218      * @param fn function that will be passed the parsed tuple values
    219      * @return object returned by {@code fn}
    220      * @throws IllegalArgumentException if the input string format is invalid
    221      */
    222     public <T> T parse(final String str, final DoubleFunction2N<T> fn) {
    223         final ParsePosition pos = new ParsePosition(0);
    224
    225         readPrefix(str, pos);
    226         final double v1 = readTupleValue(str, pos);
    227         final double v2 = readTupleValue(str, pos);
    228         readSuffix(str, pos);
    229         endParse(str, pos);
    230
    231         return fn.apply(v1, v2);
    232     }
    233
    234     /** Parse the given string as a 3-tuple and passes the parsed values to the
    235      * given function. The function output is returned.
    236      * @param <T> function return type
    237      * @param str the string to be parsed
    238      * @param fn function that will be passed the parsed tuple values
    239      * @return object returned by {@code fn}
    240      * @throws IllegalArgumentException if the input string format is invalid
    241      */
    242     public <T> T parse(final String str, final DoubleFunction3N<T> fn) {
    243         final ParsePosition pos = new ParsePosition(0);
    244
    245         readPrefix(str, pos);
    246         final double v1 = readTupleValue(str, pos);
    247         final double v2 = readTupleValue(str, pos);
    248         final double v3 = readTupleValue(str, pos);
    249         readSuffix(str, pos);
    250         endParse(str, pos);
    251
    252         return fn.apply(v1, v2, v3);
    253     }
    254
    255     /** Read the configured prefix from the current position in the given string, ignoring any preceding
    256      * whitespace, and advance the parsing position past the prefix sequence. An exception is thrown if the
    257      * prefix is not found. Does nothing if the prefix is null.
    258      * @param str the string being parsed
    259      * @param pos the current parsing position
    260      * @throws IllegalArgumentException if the configured prefix is not null and is not found at the current
    261      *      parsing position, ignoring preceding whitespace
    262      */
    263     private void readPrefix(final String str, final ParsePosition pos) {
    264         if (prefix != null) {
    265             consumeWhitespace(str, pos);
    266             readSequence(str, prefix, pos);
    267         }
    268     }
    269
    270     /** Read and return a tuple value from the current position in the given string. An exception is thrown if a
    271      * valid number is not found. The parsing position is advanced past the parsed number and any trailing separator.
    272      * @param str the string being parsed
    273      * @param pos the current parsing position
    274      * @return the tuple value
    275      * @throws IllegalArgumentException if the configured prefix is not null and is not found at the current
    276      *      parsing position, ignoring preceding whitespace
    277      */
    278     private double readTupleValue(final String str, final ParsePosition pos) {
    279         final int startIdx = pos.getIndex();
    280
    281         int endIdx = str.indexOf(separator, startIdx);
    282         if (endIdx < 0) {
    283             if (suffix != null) {
    284                 endIdx = str.indexOf(suffix, startIdx);
    285             }
    286
    287             if (endIdx < 0) {
    288                 endIdx = str.length();
    289             }
    290         }
    291
    292         final String substr = str.substring(startIdx, endIdx);
    293         try {
    294             final double value = Double.parseDouble(substr);
    295
    296             // advance the position and move past any terminating separator
    297             pos.setIndex(endIdx);
    298             matchSequence(str, separator, pos);
    299
    300             return value;
    301         } catch (final NumberFormatException exc) {
    302             throw parseFailure(String.format("unable to parse number from string \"%s\"", substr), str, pos, exc);
    303         }
    304     }
    305
    306     /** Read the configured suffix from the current position in the given string, ignoring any preceding
    307      * whitespace, and advance the parsing position past the suffix sequence. An exception is thrown if the
    308      * suffix is not found. Does nothing if the suffix is null.
    309      * @param str the string being parsed
    310      * @param pos the current parsing position
    311      * @throws IllegalArgumentException if the configured suffix is not null and is not found at the current
    312      *      parsing position, ignoring preceding whitespace
    313      */
    314     private void readSuffix(final String str, final ParsePosition pos) {
    315         if (suffix != null) {
    316             consumeWhitespace(str, pos);
    317             readSequence(str, suffix, pos);
    318         }
    319     }
    320
    321     /** End a parse operation by ensuring that all non-whitespace characters in the string have been parsed. An
    322      * exception is thrown if extra content is found.
    323      * @param str the string being parsed
    324      * @param pos the current parsing position
    325      * @throws IllegalArgumentException if extra non-whitespace content is found past the current parsing position
    326      */
    327     private void endParse(final String str, final ParsePosition pos) {
    328         consumeWhitespace(str, pos);
    329         if (pos.getIndex() != str.length()) {
    330             throw parseFailure("unexpected content", str, pos);
    331         }
    332     }
    333
    334     /** Advance {@code pos} past any whitespace characters in {@code str},
    335      * starting at the current parse position index.
    336      * @param str the input string
    337      * @param pos the current parse position
    338      */
    339     private void consumeWhitespace(final String str, final ParsePosition pos) {
    340         int idx = pos.getIndex();
    341         final int len = str.length();
    342
    343         for (; idx < len; ++idx) {
    344             if (!Character.isWhitespace(str.codePointAt(idx))) {
    345                 break;
    346             }
    347         }
    348
    349         pos.setIndex(idx);
    350     }
    351
    352     /** Return a boolean indicating whether or not the input string {@code str}
    353      * contains the string {@code seq} at the given parse index. If the match succeeds,
    354      * the index of {@code pos} is moved to the first character after the match. If
    355      * the match does not succeed, the parse position is left unchanged.
    356      * @param str the string to match against
    357      * @param seq the sequence to look for in {@code str}
    358      * @param pos the parse position indicating the index in {@code str}
    359      *      to attempt the match
    360      * @return true if {@code str} contains exactly the same characters as {@code seq}
    361      *      at {@code pos}; otherwise, false
    362      */
    363     private boolean matchSequence(final String str, final String seq, final ParsePosition pos) {
    364         final int idx = pos.getIndex();
    365         final int inputLength = str.length();
    366         final int seqLength = seq.length();
    367
    368         int i = idx;
    369         int s = 0;
    370         for (; i < inputLength && s < seqLength; ++i, ++s) {
    371             if (str.codePointAt(i) != seq.codePointAt(s)) {
    372                 break;
    373             }
    374         }
    375
    376         if (i <= inputLength && s == seqLength) {
    377             pos.setIndex(idx + seqLength);
    378             return true;
    379         }
    380         return false;
    381     }
    382
    383     /** Read the string given by {@code seq} from the given position in {@code str}.
    384      * Throws an IllegalArgumentException if the sequence is not found at that position.
    385      * @param str the string to match against
    386      * @param seq the sequence to look for in {@code str}
    387      * @param pos the parse position indicating the index in {@code str}
    388      *      to attempt the match
    389      * @throws IllegalArgumentException if {@code str} does not contain the characters from
    390      *      {@code seq} at position {@code pos}
    391      */
    392     private void readSequence(final String str, final String seq, final ParsePosition pos) {
    393         if (!matchSequence(str, seq, pos)) {
    394             final int idx = pos.getIndex();
    395             final String actualSeq = str.substring(idx, Math.min(str.length(), idx + seq.length()));
    396
    397             throw parseFailure(String.format("expected \"%s\" but found \"%s\"", seq, actualSeq), str, pos);
    398         }
    399     }
    400
    401     /** Return an instance configured with default values. Tuples in this format
    402      * are enclosed by parentheses and separated by commas.
    403      *
    404      * Ex:
    405      * <pre>
    406      * "(1.0)"
    407      * "(1.0, 2.0)"
    408      * "(1.0, 2.0, 3.0)"
    409      * </pre>
    410      * @return instance configured with default values
    411      */
    412     public static SimpleTupleFormat getDefault() {
    413         return DEFAULT_INSTANCE;
    414     }
    415
    416     /** Return an {@link IllegalArgumentException} representing a parsing failure.
    417      * @param msg the error message
    418      * @param str the string being parsed
    419      * @param pos the current parse position
    420      * @return an exception signaling a parse failure
    421      */
    422     private static IllegalArgumentException parseFailure(final String msg, final String str, final ParsePosition pos) {
    423         return parseFailure(msg, str, pos, null);
    424     }
    425
    426     /** Return an {@link IllegalArgumentException} representing a parsing failure.
    427      * @param msg the error message
    428      * @param str the string being parsed
    429      * @param pos the current parse position
    430      * @param cause the original cause of the error
    431      * @return an exception signaling a parse failure
    432      */
    433     private static IllegalArgumentException parseFailure(final String msg, final String str, final ParsePosition pos,
    434                                                          final Throwable cause) {
    435         final String fullMsg = String.format("Failed to parse string \"%s\" at index %d: %s",
    436                 str, pos.getIndex(), msg);
    437
    438         return new TupleParseException(fullMsg, cause);
    439     }
    440
    441     /** Exception class for errors occurring during tuple parsing.
    442      */
    443     private static class TupleParseException extends IllegalArgumentException {
    444
    445         /** Serializable version identifier. */
    446         private static final long serialVersionUID = 20180629;
    447
    448         /** Simple constructor.
    449          * @param msg the exception message
    450          * @param cause the exception root cause
    451          */
    452         TupleParseException(final String msg, final Throwable cause) {
    453             super(msg, cause);
    454         }
    455     }
    456 }