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 }