View Javadoc
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 }