1 /*
2 * Copyright (C) 2018 Alberto Irurueta Carro (alberto@irurueta.com)
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.irurueta.units;
18
19 import java.text.MessageFormat;
20 import java.text.ParseException;
21 import java.util.Locale;
22 import java.util.regex.Pattern;
23
24 /**
25 * Formats and parses angle value and unit.
26 */
27 public class AngleFormatter extends MeasureFormatter<Angle, AngleUnit> {
28
29 /**
30 * Degree symbol.
31 */
32 public static final String DEGREE = "º";
33
34 /**
35 * Radian symbol.
36 */
37 public static final String RADIAN = "rad";
38
39 /**
40 * Pattern to format degrees and minutes.
41 */
42 private static final String DEGREES_AND_MINUTES_MESSAGE_PATTERN = "{0}º {1}''";
43
44 /**
45 * Pattern to format degrees, minutes and seconds.
46 */
47 private static final String DEGREES_MINUTES_AND_SECONDS_MESSAGE_PATTERN = "{0}º {1}'' {2}\"";
48
49 /**
50 * Pattern to parse degrees and minutes expressions.
51 */
52 private static final String DEGREES_AND_MINUTES_PATTERN = "^(-?\\d+)º(\\s+)(\\d+)'";
53
54 /**
55 * Pattern to parse degrees, minutes and seconds expressions.
56 */
57 private static final String DEGREES_MINUTES_AND_SECONDS_PATTERN = "^(-?\\d+)º(\\s+)(\\d+)'(\\s+)(\\d+)\"";
58
59 /**
60 * Pattern to parse angle in degrees and decimal minutes format.
61 */
62 private Pattern degreesAndMinutesPattern;
63
64 /**
65 * Pattern to parse angle in degrees, minutes and decimal seconds format.
66 */
67 private Pattern degreesMinutesAndSecondsPattern;
68
69 /**
70 * Constructor.
71 */
72 public AngleFormatter() {
73 super();
74 }
75
76 /**
77 * Constructor with locale.
78 *
79 * @param locale locale.
80 * @throws IllegalArgumentException if locale is null.
81 */
82 public AngleFormatter(final Locale locale) {
83 super(locale);
84 }
85
86 /**
87 * Copy constructor.
88 *
89 * @param formatter input instance to copy from.
90 * @throws NullPointerException if provided formatter is null.
91 */
92 public AngleFormatter(final AngleFormatter formatter) {
93 this(formatter.getLocale());
94 degreesAndMinutesPattern = formatter.degreesAndMinutesPattern;
95 degreesMinutesAndSecondsPattern = formatter.degreesMinutesAndSecondsPattern;
96 }
97
98 /**
99 * Determines if two angle formatters are equal by comparing all of its internal parameters.
100 *
101 * @param obj another object to compare.
102 * @return true if provided object is assumed to be equal to this instance.
103 */
104 @Override
105 public boolean equals(final Object obj) {
106 final var equals = super.equals(obj);
107 return (obj instanceof AngleFormatter) && equals;
108 }
109
110 /**
111 * Hash code generated for this instance. Hash codes can be internally used by some collections to coarsely
112 * compare objects. This implementation only calls parent implementation to avoid static analyzer warning.
113 *
114 * @return hash code.
115 */
116 @Override
117 public int hashCode() {
118 return super.hashCode();
119 }
120
121 /**
122 * Gets unit system for detected unit into provided string representation of a measurement.
123 *
124 * @param source a measurement string representation to be checked.
125 * @return a unit system (metric) or null if unit cannot be determined.
126 */
127 @Override
128 public UnitSystem getUnitSystem(final String source) {
129 final var unit = findUnit(source);
130 return unit != null ? UnitSystem.METRIC : null;
131 }
132
133 /**
134 * Parses provided string and tries to determine an angle value and unit.
135 *
136 * @param source a string to be parsed.
137 * @return an angle containing a value and unit.
138 * @throws ParseException if provided string cannot be parsed.
139 * @throws UnknownUnitException if unit cannot be determined.
140 */
141 @Override
142 public Angle parse(final String source) throws ParseException, UnknownUnitException {
143 return internalParse(source, new Angle());
144 }
145
146 /**
147 * Attempts to determine an angle unit within a measurement string representation.
148 *
149 * @param source an angle measurement string representation.
150 * @return an angle unit, or null if none can be determined.
151 */
152 @Override
153 public AngleUnit findUnit(final String source) {
154 if (source.contains(DEGREE + " ") || source.endsWith(DEGREE)) {
155 return AngleUnit.DEGREES;
156 }
157 if (source.contains(RADIAN + " ") || source.endsWith(RADIAN)) {
158 return AngleUnit.RADIANS;
159 }
160 return null;
161 }
162
163 /**
164 * Formats and converts provided angle value and unit using metric system. This implementation ignores provided
165 * unit system.
166 *
167 * @param value a measurement value.
168 * @param unit a measurement unit.
169 * @param system ignored.
170 * @return a string representation of angle value and unit.
171 */
172 @Override
173 public String formatAndConvert(final Number value, final AngleUnit unit, final UnitSystem system) {
174 return format(value, unit);
175 }
176
177 /**
178 * Returns unit string representation.
179 *
180 * @param unit an angle unit.
181 * @return its string representation.
182 */
183 @Override
184 public String getUnitSymbol(final AngleUnit unit) {
185 if (unit == AngleUnit.DEGREES) {
186 return DEGREE;
187 } else {
188 return RADIAN;
189 }
190 }
191
192 /**
193 * Formats provided angle into degrees and decimal minutes.
194 *
195 * @param angle angle to be formatted.
196 * @return string representation of provided angle expressed as degrees and decimal minutes.
197 */
198 public String formatDegreesAndMinutes(final Angle angle) {
199 final var degreesAndMinutes = AngleConverter.toDegreesAndMinutes(angle);
200 return MessageFormat.format(DEGREES_AND_MINUTES_MESSAGE_PATTERN,
201 numberFormat.format(degreesAndMinutes[0]),
202 numberFormat.format(degreesAndMinutes[1]));
203 }
204
205 /**
206 * Formats provided angle into degrees, minutes and decimal seconds.
207 *
208 * @param angle angle to be formatted.
209 * @return string representation of provided angle expressed as degrees, minutes and decimal seconds.
210 */
211 public String formatDegreesMinutesAndSeconds(final Angle angle) {
212 final var degreesMinutesAndSeconds = AngleConverter.toDegreesMinutesAndSeconds(angle);
213 return MessageFormat.format(DEGREES_MINUTES_AND_SECONDS_MESSAGE_PATTERN,
214 numberFormat.format(degreesMinutesAndSeconds[0]),
215 numberFormat.format(degreesMinutesAndSeconds[1]),
216 numberFormat.format(degreesMinutesAndSeconds[2]));
217 }
218
219 /**
220 * Parses provided string representation using degrees and decimal minutes format. Note: decimals are not
221 * accepted on degrees value.
222 *
223 * @param source string to be parsed.
224 * @return parsed angle.
225 * @throws ParseException if parsing fails.
226 * @throws UnknownUnitException if format is not recognized.
227 */
228 @SuppressWarnings("Duplicates")
229 public Angle parseDegreesAndMinutes(final CharSequence source) throws ParseException, UnknownUnitException {
230 if (degreesAndMinutesPattern == null) {
231 degreesAndMinutesPattern = Pattern.compile(DEGREES_AND_MINUTES_PATTERN);
232 }
233
234 final var matcher = degreesAndMinutesPattern.matcher(source);
235 if (!matcher.matches()) {
236 throw new UnknownUnitException();
237 }
238
239 final var degreeString = matcher.group(1);
240 final var minuteString = matcher.group(3);
241
242 final var degree = numberFormat.parse(degreeString);
243 final var minute = numberFormat.parse(minuteString);
244
245 final var angle = AngleConverter.fromDegreesAndMinutes(
246 degree.intValue(), minute.doubleValue(), AngleUnit.DEGREES);
247 return new Angle(angle, AngleUnit.DEGREES);
248 }
249
250 /**
251 * Parses provided string representation using degrees, minutes and decimal seconds format.
252 *
253 * @param source string to be parsed.
254 * @return parsed angle.
255 * @throws ParseException if parsing fails.
256 * @throws UnknownUnitException if format is not recognized.
257 */
258 @SuppressWarnings("Duplicates")
259 public Angle parseDegreesMinutesAndSeconds(final CharSequence source) throws ParseException, UnknownUnitException {
260 if (degreesMinutesAndSecondsPattern == null) {
261 degreesMinutesAndSecondsPattern = Pattern.compile(DEGREES_MINUTES_AND_SECONDS_PATTERN);
262 }
263
264 final var matcher = degreesMinutesAndSecondsPattern.matcher(source);
265 if (!matcher.matches()) {
266 throw new UnknownUnitException();
267 }
268
269 final var degreeString = matcher.group(1);
270 final var minuteString = matcher.group(3);
271 final var secondString = matcher.group(5);
272
273 final var degree = numberFormat.parse(degreeString);
274 final var minute = numberFormat.parse(minuteString);
275 final var second = numberFormat.parse(secondString);
276
277 final var angle = AngleConverter.fromDegreesMinutesAndSeconds(
278 degree.intValue(), minute.intValue(), second.doubleValue(), AngleUnit.DEGREES);
279 return new Angle(angle, AngleUnit.DEGREES);
280 }
281 }