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 }