0001 /*
0002 * Java GPX Library (jpx-3.1.0).
0003 * Copyright (c) 2016-2023 Franz Wilhelmstötter
0004 *
0005 * Licensed under the Apache License, Version 2.0 (the "License");
0006 * you may not use this file except in compliance with the License.
0007 * You may obtain a copy of the License at
0008 *
0009 * http://www.apache.org/licenses/LICENSE-2.0
0010 *
0011 * Unless required by applicable law or agreed to in writing, software
0012 * distributed under the License is distributed on an "AS IS" BASIS,
0013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0014 * See the License for the specific language governing permissions and
0015 * limitations under the License.
0016 *
0017 * Author:
0018 * Franz Wilhelmstötter (franz.wilhelmstoetter@gmail.com)
0019 */
0020 package io.jenetics.jpx.format;
0021
0022 import static java.util.Objects.requireNonNull;
0023
0024 import java.text.ParsePosition;
0025 import java.util.ArrayList;
0026 import java.util.Iterator;
0027 import java.util.List;
0028 import java.util.Optional;
0029 import java.util.Set;
0030 import java.util.stream.Collectors;
0031
0032 import io.jenetics.jpx.Latitude;
0033 import io.jenetics.jpx.Length;
0034 import io.jenetics.jpx.Longitude;
0035
0036 /**
0037 * Formatter for printing and parsing geographic location objects.
0038 * <p>
0039 * <b>Patterns for Formatting and Parsing</b>
0040 * <p>
0041 * Patterns are based on a simple sequence of letters and symbols. A pattern is
0042 * used to create a Formatter using the {@code #ofPattern(String)} and
0043 * {@code #ofPattern(String, Locale)} methods.
0044 *
0045 * For example, {@code D°MM'SS.SSS"X} will format to {@code 60°15'59.613"N}.
0046 *
0047 * A formatter created from a pattern can be used as many times as necessary, it
0048 * is immutable and is thread-safe.
0049 *
0050 * <table class="striped">
0051 * <caption><b>Pattern Letters and Symbols</b></caption>
0052 * <thead>
0053 * <tr><th scope="col">Symbol</th><th scope="col">Meaning</th><th scope="col">Examples</th>
0054 * </thead>
0055 * <tbody>
0056 * <tr><th scope="row">L</th><td>deprecated synonym for 'D'</td></tr>
0057 * <tr>
0058 * <th scope="row">D</th>
0059 * <td>Latitude in degrees. Values in {@code -90 <= d <= +90}. See examples.</td>
0060 * <td>34; 23.2332</td>
0061 * </tr>
0062 * <tr>
0063 * <th scope="row">M</th>
0064 * <td>Latitude minutes. Values in {@code 0 <= m < 60}. See examples.</td>
0065 * <td>45; 45.6</td>
0066 * </tr>
0067 * <tr>
0068 * <th scope="row">S</th>
0069 * <td>Latitude seconds. Values in {@code 0 <= s < 60}. See examples.</td>
0070 * <td>7; 07</td>
0071 * </tr>
0072 * <tr>
0073 * <th scope="row">X</th>
0074 * <td>hemisphere (N or S)</td>
0075 * <td>N; S</td>
0076 * </tr>
0077 * <tr><th scope="row">l</th><td>deprecated synonym for 'd'</td></tr>
0078 * <tr>
0079 * <th scope="row">d</th>
0080 * <td>Longitude degrees. Values in {@code -180 <= d <= +180}. Similar to Latitude degrees.</td>
0081 * <td>34; 23.2332</td>
0082 * </tr>
0083 * <tr>
0084 * <th scope="row">m</th>
0085 * <td>Longitude minutes. Similar to latitude minutes.</td>
0086 * <td>45; 45.6</td>
0087 * </tr>
0088 * <tr>
0089 * <th scope="row">s</th>
0090 * <td>Longitude seconds. Similar to Latitude seconds.</td>
0091 * <td>7; 07</td>
0092 * </tr>
0093 * <tr>
0094 * <th scope="row">x</th>
0095 * <td>hemisphere (E or W)</td>
0096 * <td>E; W</td>
0097 * </tr>
0098 * <tr>
0099 * <th scope="row">E</th>
0100 * <td>Elevation in meters. See examples.</td>
0101 * <td>234; 1023; -12</td>
0102 * </tr>
0103 * <tr><th scope="row">H</th><td>deprecated synonym for 'E'</td></tr>
0104 * <tr>
0105 * <th scope="row">'</th>
0106 * <td>escape for text</td>
0107 * <td></td>
0108 * </tr>
0109 * <tr>
0110 * <th scope="row">''</th>
0111 * <td>single quote</td>
0112 * <td>'</td>
0113 * </tr>
0114 * <tr>
0115 * <th scope="row">[</th>
0116 * <td>optional section start</td>
0117 * <td></td>
0118 * </tr>
0119 * <tr>
0120 * <th scope="row">]</th>
0121 * <td>optional section end</td>
0122 * <td></td>
0123 * </tr>
0124 * </tbody>
0125 * </table>
0126 *
0127 * <table class="striped">
0128 * <caption><b>Examples</b></caption>
0129 * <thead>
0130 * <tr>
0131 * <th scope="col">Pattern</th>
0132 * <th scope="col">Meaning</th>
0133 * <th scope="col">Examples</th>
0134 * </thead>
0135 * <tbody>
0136 * <tr>
0137 * <th scope="row">+D</th>
0138 * <td>Latitude sign indicated by prefix +/-</td>
0139 * <td>+47; -24</td>
0140 * </tr>
0141 * <tr>
0142 * <th scope="row">D X</th>
0143 * <td>Latitude sign indicated by X</td>
0144 * <td>47 N; 24 S</td>
0145 * </tr>
0146 * <tr>
0147 * <th scope="row">D.DD</th>
0148 * <td>Latitude degrees with decimal faction</td>
0149 * <td>47.50</td>
0150 * </tr>
0151 * <tr>
0152 * <th scope="row">D M</th>
0153 * <td>Latitude in degrees and minutes</td>
0154 * <td>47 30</td>
0155 * </tr>
0156 * <tr>
0157 * <th scope="row">D</th>
0158 * <td>Latitude degrees in variable width</td>
0159 * <td>9; 47</td>
0160 * </tr>
0161 * <tr>
0162 * <th scope="row">DD</th>
0163 * <td>Latitude degrees in fixed width</td>
0164 * <td>09; 47</td>
0165 * </tr>
0166 * <tr>
0167 * <th scope="row">D M.MM</th>
0168 * <td>Latitude minutes with decimal fraction</td>
0169 * <td>47 2.25</td>
0170 * </tr>
0171 * <tr>
0172 * <th scope="row">D M S</th>
0173 * <td>Latitude minutes and seconds</td>
0174 * <td>47 2 15</td>
0175 * </tr>
0176 * <tr>
0177 * <th scope="row">D M</th>
0178 * <td>Latitude minutes in variable width</td>
0179 * <td>47 2; 46 12</td>
0180 * </tr>
0181 * <tr>
0182 * <th scope="row">D MM</th>
0183 * <td>Latitude minutes in fixed width</td>
0184 * <td>47 02; 46 12</td>
0185 * </tr>
0186 * <tr>
0187 * <th scope="row">D M S</th>
0188 * <td>Latitude seconds in variable width</td>
0189 * <td>47 2 3; 46 2 13</td>
0190 * </tr>
0191 * <tr>
0192 * <th scope="row">D M SS</th>
0193 * <td>Latitude seconds in fixed width</td>
0194 * <td>47 2 03; 46 2 13</td>
0195 * </tr>
0196 * <tr>
0197 * <th scope="row">D M S.SS</th>
0198 * <td>Latitude seconds with decimal fraction</td>
0199 * <td>46 2 13.54</td>
0200 * </tr>
0201 * <tr>
0202 * <th scope="row">+d</th>
0203 * <td>Longitude sign indicated by prefix +/-</td>
0204 * <td>+147; -124</td>
0205 * </tr>
0206 * <tr>
0207 * <th scope="row">d x</th>
0208 * <td>Latitude sign indicated by x</td>
0209 * <td>147 E; 124 W</td>
0210 * </tr>
0211 * <tr>
0212 * <th scope="row">d</th>
0213 * <td>Longitude degrees in variable width</td>
0214 * <td>9; 47; 175</td>
0215 * </tr>
0216 * <tr>
0217 * <th scope="row">ddd</th>
0218 * <td>Longitude degrees in fixed width</td>
0219 * <td>009; 047; 175</td>
0220 * </tr>
0221 * <tr>
0222 * <th scope="row">d m.mm</th>
0223 * <td>Longitude minutes with decimal fraction</td>
0224 * <td>47 2.25</td>
0225 * </tr>
0226 * <tr>
0227 * <th scope="row">d m s</th>
0228 * <td>Longitude minutes and seconds</td>
0229 * <td>47 2 15</td>
0230 * </tr>
0231 * <tr>
0232 * <th scope="row">d m</th>
0233 * <td>Longitude minutes in variable width</td>
0234 * <td>47 2; 46 12</td>
0235 * </tr>
0236 * <tr>
0237 * <th scope="row">d mm</th>
0238 * <td>Longitude minutes in fixed width</td>
0239 * <td>47 02; 46 12</td>
0240 * </tr>
0241 * <tr>
0242 * <th scope="row">d m s</th>
0243 * <td>Longitude seconds in variable width</td>
0244 * <td>47 2 3; 46 2 13</td>
0245 * </tr>
0246 * <tr>
0247 * <th scope="row">d m ss</th>
0248 * <td>Longitude seconds in fixed width</td>
0249 * <td>47 2 03; 46 2 13</td>
0250 * </tr>
0251 * <tr>
0252 * <th scope="row">d m s.ss</th>
0253 * <td>Longitude seconds with decimal fraction</td>
0254 * <td>46 2 13.54</td>
0255 * </tr>
0256 * <tr>
0257 * <th scope="row">E</th>
0258 * <td>Elevation without positive sign</td>
0259 * <td>9; -9</td>
0260 * </tr>
0261 * <tr>
0262 * <th scope="row">+E</th>
0263 * <td>Elevation with positive or negative sign</td>
0264 * <td>+9; -9</td>
0265 * </tr>
0266 * <tr>
0267 * <th scope="row">E</th>
0268 * <td>Elevation in variable width</td>
0269 * <td>9; 19; 190</td>
0270 * </tr>
0271 * <tr>
0272 * <th scope="row">EEE</th>
0273 * <td>Elevation in fixed width</td>
0274 * <td>009; 019; 190</td>
0275 * </tr>
0276 * <tr>
0277 * <th scope="row">E.EEE</th>
0278 * <td>Elevation with decimal fraction</td>
0279 * <td>9.123</td>
0280 * </tr>
0281 * </tbody>
0282 * </table>
0283 *
0284 * @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
0285 * @version 2.2
0286 * @since 1.4
0287 */
0288 public final class LocationFormatter {
0289
0290 static final Set<Character> PROTECTED_CHARS = Set.of(
0291 'L', 'D', 'M', 'S', 'l', 'd', 'm', 's', 'E', 'H', 'X', 'x', '+', '[', ']'
0292 );
0293
0294 /**
0295 * Latitude formatter with the pattern <em>{@code D°MM''SS.SSS"X}</em>.
0296 * Example: <em>{@code 16°27'59.180"N}</em>.
0297 */
0298 public static final LocationFormatter ISO_HUMAN_LAT_LONG =
0299 ofPattern("D°MM''SS.SSS\"X");
0300
0301 /**
0302 * Longitude formatter with the pattern <em>{@code d°mm''ss.sss"x}</em>.
0303 * Example: <em>{@code 16°27'59.180"E}</em>.
0304 */
0305 public static final LocationFormatter ISO_HUMAN_LON_LONG =
0306 ofPattern("d°mm''ss.sss\"x");
0307
0308 /**
0309 * Elevation formatter with the pattern <em>{@code E.EE'm'}</em>. Example:
0310 * <em>{@code 2045m}</em>.
0311 */
0312 public static final LocationFormatter ISO_HUMAN_ELE_LONG =
0313 ofPattern("E.EE'm'");
0314
0315 /**
0316 * Elevation formatter with the pattern
0317 * <em>{@code D°MM''SS.SSS"X d°mm''ss.sss"x[ E.EE'm']}</em>.
0318 * Example: <em>{@code 50°03′46.461″S 125°48′26.533″E 978.90m}</em>.
0319 */
0320 public static final LocationFormatter ISO_HUMAN_LONG =
0321 ofPattern("D°MM''SS.SSS\"X d°mm''ss.sss\"x[ E.EE'm']");
0322
0323 /**
0324 * ISO 6709 conform latitude format, short: <em>{@code +DD.DD}</em>.
0325 */
0326 public static final LocationFormatter ISO_LAT_SHORT = ofPattern("+DD.DD");
0327
0328 /**
0329 * ISO 6709 conform latitude format, medium: <em>{@code +DDMM.MMM}</em>.
0330 */
0331 public static final LocationFormatter ISO_LAT_MEDIUM = ofPattern("+DDMM.MMM");
0332
0333 /**
0334 * ISO 6709 conform latitude format, long: <em>{@code +DDMMSS.SS}</em>.
0335 */
0336 public static final LocationFormatter ISO_LAT_LONG = ofPattern("+DDMMSS.SS");
0337
0338 /**
0339 * ISO 6709 conform longitude format, short: <em>{@code +ddd.dd}</em>.
0340 */
0341 public static final LocationFormatter ISO_LON_SHORT = ofPattern("+ddd.dd");
0342
0343 /**
0344 * ISO 6709 conform longitude format, medium: <em>{@code +dddmm.mmm}</em>.
0345 */
0346 public static final LocationFormatter ISO_LON_MEDIUM = ofPattern("+dddmm.mmm");
0347
0348 /**
0349 * ISO 6709 conform longitude format, long: <em>{@code +dddmmss.ss}</em>.
0350 */
0351 public static final LocationFormatter ISO_LON_LONG = ofPattern("+dddmmss.ss");
0352
0353 /**
0354 * ISO 6709 conform elevation format, short: <em>{@code +E'CRS'}</em>.
0355 */
0356 public static final LocationFormatter ISO_ELE_SHORT = ofPattern("+E'CRS'");
0357
0358 /**
0359 * ISO 6709 conform elevation format, medium: <em>{@code +E.E'CRS'}</em>.
0360 */
0361 public static final LocationFormatter ISO_ELE_MEDIUM = ofPattern("+E.E'CRS'");
0362
0363 /**
0364 * ISO 6709 conform elevation format, long: <em>{@code +E.EE'CRS'}</em>.
0365 */
0366 public static final LocationFormatter ISO_ELE_LONG = ofPattern("+E.EE'CRS'");
0367
0368 /**
0369 * ISO 6709 conform location format, short:
0370 * <em>{@code +DD.DD+ddd.dd[+E'CRS']}</em>.
0371 */
0372 public static final LocationFormatter ISO_SHORT =
0373 ofPattern("+DD.DD+ddd.dd[+E'CRS']");
0374
0375 /**
0376 * ISO 6709 conform location format, medium:
0377 * <em>{@code +DDMM.MMM+dddmm.mmm[+E.E'CRS']}</em>.
0378 */
0379 public static final LocationFormatter ISO_MEDIUM =
0380 ofPattern("+DDMM.MMM+dddmm.mmm[+E.E'CRS']");
0381
0382 /**
0383 * ISO 6709 conform location format, medium:
0384 * <em>{@code +DDMMSS.SS+dddmmss.ss[+E.EE'CRS']}</em>.
0385 */
0386 public static final LocationFormatter ISO_LONG =
0387 ofPattern("+DDMMSS.SS+dddmmss.ss[+E.EE'CRS']");
0388
0389 private final List<Format> _formats;
0390
0391 private LocationFormatter(final List<Format> formats) {
0392 _formats = List.copyOf(formats);
0393 }
0394
0395 /**
0396 * Formats the given {@code location} using {@code this} formatter.
0397 * @param location the location to format
0398 * @return the format string
0399 * @throws NullPointerException if the given {@code location} is {@code null}
0400 * @throws FormatterException if the formatter tries to format a non-existing,
0401 * non-optional location fields.
0402 */
0403 public String format(final Location location) {
0404 requireNonNull(location);
0405 return _formats.stream()
0406 .map(format -> format
0407 .format(location)
0408 .orElseThrow(() -> toError(location)))
0409 .collect(Collectors.joining());
0410 }
0411
0412 private FormatterException toError(final Location location) {
0413 return new FormatterException(String.format(
0414 "Invalid format '%s' for location %s.", toPattern(), location
0415 ));
0416 }
0417
0418 /**
0419 * Return the pattern string represented by this formatter.
0420 *
0421 * @see #ofPattern(String)
0422 *
0423 * @return the pattern string of {@code this} formatter
0424 */
0425 public String toPattern() {
0426 return _formats.stream()
0427 .map(Format::toPattern)
0428 .collect(Collectors.joining());
0429 }
0430
0431 /**
0432 * Formats the given location elements using {@code this} formatter.
0433 *
0434 * @see #format(Location)
0435 *
0436 * @param lat the latitude part of the location
0437 * @param lon the longitude part of the location
0438 * @param ele the elevation part of the location
0439 * @return the format string
0440 * @throws FormatterException if the formatter tries to format a non-existing,
0441 * non-optional location fields.
0442 */
0443 public String format(
0444 final Latitude lat,
0445 final Longitude lon,
0446 final Length ele
0447 ) {
0448 return format(Location.of(lat, lon, ele));
0449 }
0450
0451 /**
0452 * Formats the given location elements using {@code this} formatter.
0453 *
0454 * @see #format(Location)
0455 *
0456 * @param lat the latitude part of the location
0457 * @param lon the longitude part of the location
0458 * @return the format string
0459 * @throws FormatterException if the formatter tries to format a non-existing,
0460 * non-optional location fields.
0461 */
0462 public String format(final Latitude lat, final Longitude lon) {
0463 return format(lat, lon, null);
0464 }
0465
0466 /**
0467 * Formats the given location elements using {@code this} formatter.
0468 *
0469 * @see #format(Location)
0470 *
0471 * @param lat the latitude part of the location
0472 * @return the format string
0473 * @throws FormatterException if the formatter tries to format a non-existing,
0474 * non-optional location fields.
0475 */
0476 public String format(final Latitude lat) {
0477 return format(lat, null, null);
0478 }
0479
0480 /**
0481 * Formats the given location elements using {@code this} formatter.
0482 *
0483 * @see #format(Location)
0484 *
0485 * @param lon the longitude part of the location
0486 * @return the format string
0487 * @throws FormatterException if the formatter tries to format a non-existing,
0488 * non-optional location fields.
0489 */
0490 public String format(final Longitude lon) {
0491 return format(null, lon, null);
0492 }
0493
0494 /**
0495 * Formats the given location elements using {@code this} formatter.
0496 *
0497 * @see #format(Location)
0498 *
0499 * @param ele the elevation part of the location
0500 * @return the format string
0501 * @throws FormatterException if the formatter tries to format a non-existing,
0502 * non-optional location field.
0503 */
0504 public String format(final Length ele) {
0505 return format(null, null, ele);
0506 }
0507
0508 /**
0509 * Parses the text using this formatter, providing control over the text
0510 * position. This parses the text without requiring the parse to start from
0511 * the beginning of the string or finish at the end. The text will be parsed
0512 * from the specified start ParsePosition. The entire length of the text
0513 * does not have to be parsed, the {@link ParsePosition} will be updated with
0514 * the index at the end of parsing.
0515 *
0516 * @param text the text to parse, not null
0517 * @param pos the position to parse from, updated with length parsed and the
0518 * index of any error, not null
0519 * @return the parsed Location, not null
0520 * @throws ParseException - if unable to parse the requested result
0521 * @throws IndexOutOfBoundsException - if the position is invalid
0522 */
0523 public Location parse(final CharSequence text, final ParsePosition pos) {
0524 requireNonNull(text);
0525 requireNonNull(pos);
0526
0527 if (pos.getIndex() < 0 || text.length() <= pos.getIndex()) {
0528 throw new IndexOutOfBoundsException(pos.getIndex());
0529 }
0530
0531 final var builder = new LocationBuilder();
0532 for (var format : _formats) {
0533 format.parse(text, pos, builder);
0534 }
0535
0536 return builder.build();
0537 }
0538
0539 /**
0540 * Fully parses the text producing a location. This parses the entire text
0541 * producing a location. It is typically more useful to use
0542 * {@link #parse(CharSequence, ParsePosition)}. If the parse completes
0543 * without reading the entire length of the text, or a problem occurs during
0544 * parsing or merging, then an exception is thrown.
0545 *
0546 * @param text the text to parse, not null
0547 * @return the parsed temporal object, not null
0548 * @throws ParseException if unable to parse the requested result
0549 */
0550 public Location parse(final CharSequence text) {
0551 requireNonNull(text);
0552
0553 final var pos = new ParsePosition(0);
0554 final var location = parse(text, pos);
0555 if (pos.getIndex() != text.length()) {
0556 throw new ParseException("Not all input used", text, pos.getIndex());
0557 }
0558
0559 return location;
0560 }
0561
0562 @Override
0563 public String toString() {
0564 return String.format("LocationFormat[%s]", toPattern());
0565 }
0566
0567 /* *************************************************************************
0568 * Static factory methods.
0569 * ************************************************************************/
0570
0571 /**
0572 * Creates a formatter using the specified pattern.
0573 *
0574 * @see #toPattern()
0575 *
0576 * @param pattern the formatter pattern
0577 * @return the location-formatter of the given {@code pattern}
0578 * @throws NullPointerException if the given {@code pattern} is {@code null}
0579 * @throws IllegalArgumentException if the given {@code pattern} is invalid
0580 */
0581 public static LocationFormatter ofPattern(final String pattern) {
0582 requireNonNull(pattern);
0583
0584 return new Builder()
0585 .appendPattern(pattern)
0586 .build();
0587 }
0588
0589 /* *************************************************************************
0590 * Inner classes.
0591 * ************************************************************************/
0592
0593 /**
0594 * Builder to create location formatters. This allows a
0595 * {@code LocationFormatter} to be created. All location formatters are
0596 * created ultimately using this builder.
0597 *
0598 * @implNote
0599 * This class is a mutable builder intended for use from a single thread.
0600 *
0601 * @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
0602 * @version 1.4
0603 * @since 1.4
0604 */
0605 static class Builder {
0606
0607 /**
0608 * The formats that will go into the LocationFormatter.
0609 */
0610 private final List<Format> _formats = new ArrayList<>();
0611
0612 private Builder() {
0613 }
0614
0615 /**
0616 * Appends the elements defined by the specified pattern to the builder.
0617 *
0618 * @param pattern the pattern to add
0619 * @return {@code this}, for chaining, not {@code null}
0620 * @throws NullPointerException if the given {@code pattern} is
0621 * {@code null}
0622 * @throws IllegalArgumentException if the given {@code pattern} is
0623 * invalid
0624 */
0625 Builder appendPattern(final String pattern) {
0626 parsePattern(pattern);
0627 return this;
0628 }
0629
0630 /**
0631 * Completes this builder by creating the {@code LocationFormatter}.
0632 *
0633 * @return a new location-formatter object
0634 * @throws IllegalArgumentException invalid pattern
0635 */
0636 LocationFormatter build() {
0637 validate();
0638 return new LocationFormatter(_formats);
0639 }
0640
0641 private void validate(){
0642 LatitudeDegree D = null;
0643 LatitudeMinute M = null;
0644 LatitudeSecond S = null;
0645 LatitudeNS X = null;
0646 LongitudeDegree d = null;
0647 LongitudeMinute m = null;
0648 LongitudeSecond s = null;
0649 LongitudeEW x = null;
0650 Elevation E = null;
0651
0652 for (var format : _formats) {
0653 if (format instanceof LatitudeDegree ld) {
0654 if (D == null) {
0655 D = ld;
0656 } else {
0657 throw iae("Only one 'D' pattern allowed.");
0658 }
0659 } else if (format instanceof LatitudeMinute lm) {
0660 if (M == null) {
0661 M = lm;
0662 } else {
0663 throw iae("Only one 'M' pattern allowed.");
0664 }
0665 } else if (format instanceof LatitudeSecond ls) {
0666 if (S == null) {
0667 S = ls;
0668 } else {
0669 throw iae("Only one 'S' pattern allowed.");
0670 }
0671 } else if (format instanceof LatitudeNS && X==null) {
0672 X = (LatitudeNS)format;
0673 } else if (format instanceof LongitudeDegree ld) {
0674 if (d == null) {
0675 d = ld;
0676 } else {
0677 throw iae("Only one 'd' pattern allowed.");
0678 }
0679 } else if (format instanceof LongitudeMinute lm) {
0680 if (m == null) {
0681 m = lm;
0682 } else {
0683 throw iae("Only one 'm' pattern allowed.");
0684 }
0685 } else if (format instanceof LongitudeSecond ls) {
0686 if (s == null) {
0687 s = ls;
0688 } else {
0689 throw iae("Only one 's' pattern allowed.");
0690 }
0691 } else if (format instanceof LongitudeEW lew && x == null) {
0692 x = lew;
0693 } else if (format instanceof Elevation ele) {
0694 if (E == null) {
0695 E = ele;
0696 } else {
0697 throw iae("Only one 'E' pattern allowed.");
0698 }
0699 }
0700 }
0701
0702 // Validating latitude /////////////////////////////////////////////
0703
0704 if (D == null && M != null) {
0705 throw iae("No 'M' without 'D'.");
0706 }
0707 if (M == null && S != null) {
0708 throw iae("No 'S' without 'M'.");
0709 }
0710
0711 // If X, D without sign.
0712 if (X != null && D != null && D.isPrefixSign()) {
0713 throw iae("If 'X' in pattern, 'D' must be without '+'.");
0714 }
0715
0716 // If D has fractional, no M or S
0717 if (D != null &&
0718 0 < D.getMinimumFractionDigits()
0719 && M != null)
0720 {
0721 throw iae("If 'D' has fraction, no 'M' or 'S' allowed.");
0722 }
0723
0724 // If M has fractional, no S
0725 if (M != null &&
0726 0 < M.getMinimumFractionDigits() &&
0727 S != null)
0728 {
0729 throw iae("If 'M' has fraction, no 'S' allowed.");
0730 }
0731
0732 // Validating longitude ////////////////////////////////////////////
0733
0734 if (d == null && m != null) {
0735 throw iae("No 'm' without 'd'.");
0736 }
0737 if (m == null && s != null) {
0738 throw iae("No 's' without 'm'.");
0739 }
0740
0741 // If x, d without sign.
0742 if (x != null && d != null && d.isPrefixSign()) {
0743 throw iae("If 'x' in pattern, 'd' must be without '+'.");
0744 }
0745
0746 // If d has fractional, no m or s
0747 if (d != null &&
0748 0 < d.getMinimumFractionDigits() &&
0749 m != null)
0750 {
0751 throw iae("If 'd' has fraction, no 'm' or 's' allowed.");
0752 }
0753
0754 // If m has fractional, no s.
0755 if (m != null &&
0756 0 < m.getMinimumFractionDigits() &&
0757 s != null)
0758 {
0759 throw iae("If 'm' has fraction, no 's' allowed.");
0760 }
0761
0762 // This is still construction, not a validity check. ///////////////
0763
0764 if (X != null && D != null) {
0765 D.setAbsolute(true);
0766 }
0767 if (M != null) {
0768 D.setTruncate(true);
0769 }
0770 if (S != null) {
0771 M.setTruncate(true);
0772 }
0773
0774 if (x != null && d != null) {
0775 d.setAbsolute(true);
0776 }
0777 if (m != null) {
0778 d.setTruncate(true);
0779 }
0780 if (s != null) {
0781 m.setTruncate(true);
0782 }
0783 }
0784
0785 private static IllegalArgumentException iae(final String message) {
0786 return new IllegalArgumentException(message);
0787 }
0788
0789 private void parsePattern(final String pattern) {
0790 requireNonNull(pattern);
0791
0792 // The formats we've collected and that are not yet added to
0793 // formats. They may be added to _formats directly or be bundled
0794 // into an Optional first.
0795 final List<Format> formats = new ArrayList<>();
0796
0797 boolean optional = false; // Inside [ ] ?
0798 int signs = 0; // How many unprocessed '+' ?
0799 boolean quote = false; // last was ' ?
0800
0801 final var tokens = new Tokens(tokenize(pattern));
0802 while (tokens.hasNext()) {
0803 var token = tokens.next();
0804 switch (token) {
0805 case "X" -> {
0806 List<Format> fs = optional ? formats : _formats;
0807 for (int i = 0; i < signs; ++i) fs.add(Plus.INSTANCE);
0808 signs = 0;
0809 fs.add(LatitudeNS.INSTANCE);
0810 }
0811 case "x" -> {
0812 List<Format> fs = optional ? formats : _formats;
0813 for (int i = 0; i < signs; ++i) fs.add(Plus.INSTANCE);
0814 signs = 0;
0815 fs.add(LongitudeEW.INSTANCE);
0816 }
0817 case "+" -> ++signs;
0818 case "[" -> {
0819 if (optional) {
0820 throw iae("No nesting '[' (optional) allowed.");
0821 }
0822 for (int i = 0; i < signs; i++) {
0823 _formats.add(Plus.INSTANCE);
0824 }
0825 signs = 0;
0826 optional = true;
0827 }
0828 case "]" -> {
0829 if (!optional) {
0830 throw iae("Missing open '[' bracket.");
0831 }
0832 // Formats will be bundled into Optional and added to
0833 // _formats.
0834 for (int i = 0; i < signs; i++) {
0835 formats.add(Plus.INSTANCE);
0836 }
0837 signs = 0;
0838 optional = false;
0839 _formats.add(OptionalFormat.of(formats));
0840 formats.clear();
0841 }
0842 case "'" -> {
0843 List<Format> fs = optional ? formats : _formats;
0844 for (int i = 0; i < signs; ++i)
0845 fs.add(Plus.INSTANCE);
0846
0847 if (tokens.after().filter("'"::equals).isPresent()) {
0848 fs.add(ConstFormat.of("'"));
0849 tokens.next();
0850 break;
0851 }
0852 if (quote) {
0853 if (tokens.before().isPresent()) {
0854 fs.add(ConstFormat.of(
0855 tokens.before()
0856 .orElseThrow(AssertionError::new)
0857 ));
0858 }
0859 quote = false;
0860 } else {
0861 quote = true;
0862 }
0863 }
0864 default -> {
0865 List<Format> fs = optional ? formats : _formats;
0866 if (!quote) {
0867 final var field = Field.ofPattern(token);
0868 if (field.isPresent()) {
0869 final var f = field.get();
0870
0871 // Maybe first add some sign formats.
0872 if (0 < signs) {
0873 // One goes to the field.
0874 f.setPrefixSign(true);
0875 for (int i = 1; i < signs; i++) {
0876 // The rest will be Plus.
0877 fs.add(Plus.INSTANCE);
0878 }
0879 }
0880
0881 fs.add(f);
0882 } else {
0883 fs.add(ConstFormat.of(token));
0884 }
0885 }
0886 signs = 0;
0887 }
0888 }
0889 }
0890
0891 // Maybe there are still signs left over.
0892 for (int i = 0; i < signs; i++) {
0893 formats.add(Plus.INSTANCE);
0894 }
0895
0896 if (optional) {
0897 throw iae("No closing ']' found.");
0898 }
0899 if (quote) {
0900 throw iae("Missing closing ' character.");
0901 }
0902
0903 _formats.addAll(formats);
0904 }
0905
0906 static List<String> tokenize(final String pattern) {
0907 final var tokens = new ArrayList<String>();
0908 final var token = new StringBuilder();
0909
0910 boolean quote = false;
0911 char pc = '\0';
0912 for (int i = 0; i < pattern.length(); ++i) {
0913 final char c = pattern.charAt(i);
0914
0915 switch (c) {
0916 case '\'':
0917 quote = !quote;
0918 if (token.length() > 0) {
0919 tokens.add(token.toString());
0920 token.setLength(0);
0921 }
0922 tokens.add(Character.toString(c));
0923 break;
0924 case 'x', 'X', '+', '[', ']':
0925 if (quote) {
0926 token.append(c);
0927 } else {
0928 if (token.length() > 0) {
0929 tokens.add(token.toString());
0930 token.setLength(0);
0931 }
0932 tokens.add(Character.toString(c));
0933 }
0934 break;
0935 case 'L', 'D', 'M', 'S', 'l', 'd', 'm', 's', 'E', 'H':
0936 if (c != pc &&
0937 pc != '\0' &&
0938 pc != '.' &&
0939 pc != ',' &&
0940 !quote)
0941 {
0942 if (token.length() > 0) {
0943 tokens.add(token.toString());
0944 token.setLength(0);
0945 }
0946 }
0947 token.append(c);
0948 break;
0949 case ',', '.':
0950 token.append(c);
0951 break;
0952 default:
0953 if (PROTECTED_CHARS.contains(pc) || pc == '\'') {
0954 if (token.length() > 0) {
0955 tokens.add(token.toString());
0956 token.setLength(0);
0957 }
0958 }
0959 token.append(c);
0960 break;
0961 }
0962
0963 pc = c;
0964 }
0965
0966 if (token.length() > 0) {
0967 tokens.add(token.toString());
0968 }
0969
0970 return tokens;
0971 }
0972
0973 }
0974
0975 private static final class Tokens implements Iterator<String> {
0976 private final List<String> _tokens;
0977
0978 private int _index = 0;
0979
0980 private Tokens(final List<String> tokens) {
0981 _tokens = List.copyOf(tokens);
0982 }
0983
0984 @Override
0985 public boolean hasNext() {
0986 return _index < _tokens.size();
0987 }
0988
0989 @Override
0990 public String next() {
0991 return _tokens.get(_index++);
0992 }
0993
0994 Optional<String> before() {
0995 return _index - 1 > 0
0996 ? Optional.of(_tokens.get(_index - 2))
0997 : Optional.empty();
0998 }
0999
1000 Optional<String> after() {
1001 return hasNext()
1002 ? Optional.of(_tokens.get(_index))
1003 : Optional.empty();
1004 }
1005
1006 }
1007
1008 }
|