001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.geometry.core.internal;
018
019import java.text.ParsePosition;
020
021/** Class for performing simple formatting and parsing of real number tuples.
022 */
023public class SimpleTupleFormat {
024
025    /** Default value separator string. */
026    private static final String DEFAULT_SEPARATOR = ",";
027
028    /** Space character. */
029    private static final String SPACE = " ";
030
031    /** Static instance configured with default values. Tuples in this format
032     * are enclosed by parentheses and separated by commas.
033     */
034    private static final SimpleTupleFormat DEFAULT_INSTANCE =
035            new SimpleTupleFormat(",", "(", ")");
036
037    /** String separating tuple values. */
038    private final String separator;
039
040    /** String used to signal the start of a tuple; may be null. */
041    private final String prefix;
042
043    /** String used to signal the end of a tuple; may be null. */
044    private final String suffix;
045
046    /** Constructs a new instance with the default string separator (a comma)
047     * and the given prefix and suffix.
048     * @param prefix String used to signal the start of a tuple; if null, no
049     *      string is expected at the start of the tuple
050     * @param suffix String used to signal the end of a tuple; if null, no
051     *      string is expected at the end of the tuple
052     */
053    public SimpleTupleFormat(final String prefix, final String suffix) {
054        this(DEFAULT_SEPARATOR, prefix, suffix);
055    }
056
057    /** Simple constructor.
058     * @param separator String used to separate tuple values; must not be null.
059     * @param prefix String used to signal the start of a tuple; if null, no
060     *      string is expected at the start of the tuple
061     * @param suffix String used to signal the end of a tuple; if null, no
062     *      string is expected at the end of the tuple
063     */
064    protected SimpleTupleFormat(final String separator, final String prefix, final String suffix) {
065        this.separator = separator;
066        this.prefix = prefix;
067        this.suffix = suffix;
068    }
069
070    /** Return the string used to separate tuple values.
071     * @return the value separator string
072     */
073    public String getSeparator() {
074        return separator;
075    }
076
077    /** Return the string used to signal the start of a tuple. This value may be null.
078     * @return the string used to begin each tuple or null
079     */
080    public String getPrefix() {
081        return prefix;
082    }
083
084    /** Returns the string used to signal the end of a tuple. This value may be null.
085     * @return the string used to end each tuple or null
086     */
087    public String getSuffix() {
088        return suffix;
089    }
090
091    /** Return a tuple string with the given value.
092     * @param a value
093     * @return 1-tuple string
094     */
095    public String format(final double a) {
096        final StringBuilder sb = new StringBuilder();
097
098        if (prefix != null) {
099            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}