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}