AngleFormatter.java

/*
 * Copyright (C) 2018 Alberto Irurueta Carro (alberto@irurueta.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.irurueta.units;

import java.text.MessageFormat;
import java.text.ParseException;
import java.util.Locale;
import java.util.regex.Pattern;

/**
 * Formats and parses angle value and unit.
 */
public class AngleFormatter extends MeasureFormatter<Angle, AngleUnit> {

    /**
     * Degree symbol.
     */
    public static final String DEGREE = "º";

    /**
     * Radian symbol.
     */
    public static final String RADIAN = "rad";

    /**
     * Pattern to format degrees and minutes.
     */
    private static final String DEGREES_AND_MINUTES_MESSAGE_PATTERN = "{0}º {1}''";

    /**
     * Pattern to format degrees, minutes and seconds.
     */
    private static final String DEGREES_MINUTES_AND_SECONDS_MESSAGE_PATTERN = "{0}º {1}'' {2}\"";

    /**
     * Pattern to parse degrees and minutes expressions.
     */
    private static final String DEGREES_AND_MINUTES_PATTERN = "^(-?\\d+)º(\\s+)(\\d+)'";

    /**
     * Pattern to parse degrees, minutes and seconds expressions.
     */
    private static final String DEGREES_MINUTES_AND_SECONDS_PATTERN = "^(-?\\d+)º(\\s+)(\\d+)'(\\s+)(\\d+)\"";

    /**
     * Pattern to parse angle in degrees and decimal minutes format.
     */
    private Pattern degreesAndMinutesPattern;

    /**
     * Pattern to parse angle in degrees, minutes and decimal seconds format.
     */
    private Pattern degreesMinutesAndSecondsPattern;

    /**
     * Constructor.
     */
    public AngleFormatter() {
        super();
    }

    /**
     * Constructor with locale.
     *
     * @param locale locale.
     * @throws IllegalArgumentException if locale is null.
     */
    public AngleFormatter(final Locale locale) {
        super(locale);
    }

    /**
     * Copy constructor.
     *
     * @param formatter input instance to copy from.
     * @throws NullPointerException if provided formatter is null.
     */
    public AngleFormatter(final AngleFormatter formatter) {
        this(formatter.getLocale());
        degreesAndMinutesPattern = formatter.degreesAndMinutesPattern;
        degreesMinutesAndSecondsPattern = formatter.degreesMinutesAndSecondsPattern;
    }

    /**
     * Determines if two angle formatters are equal by comparing all of its internal parameters.
     *
     * @param obj another object to compare.
     * @return true if provided object is assumed to be equal to this instance.
     */
    @Override
    public boolean equals(final Object obj) {
        final var equals = super.equals(obj);
        return (obj instanceof AngleFormatter) && equals;
    }

    /**
     * Hash code generated for this instance. Hash codes can be internally used by some collections to coarsely
     * compare objects. This implementation only calls parent implementation to avoid static analyzer warning.
     *
     * @return hash code.
     */
    @Override
    public int hashCode() {
        return super.hashCode();
    }

    /**
     * Gets unit system for detected unit into provided string representation of a measurement.
     *
     * @param source a measurement string representation to be checked.
     * @return a unit system (metric) or null if unit cannot be determined.
     */
    @Override
    public UnitSystem getUnitSystem(final String source) {
        final var unit = findUnit(source);
        return unit != null ? UnitSystem.METRIC : null;
    }

    /**
     * Parses provided string and tries to determine an angle value and unit.
     *
     * @param source a string to be parsed.
     * @return an angle containing a value and unit.
     * @throws ParseException       if provided string cannot be parsed.
     * @throws UnknownUnitException if unit cannot be determined.
     */
    @Override
    public Angle parse(final String source) throws ParseException, UnknownUnitException {
        return internalParse(source, new Angle());
    }

    /**
     * Attempts to determine an angle unit within a measurement string representation.
     *
     * @param source an angle measurement string representation.
     * @return an angle unit, or null if none can be determined.
     */
    @Override
    public AngleUnit findUnit(final String source) {
        if (source.contains(DEGREE + " ") || source.endsWith(DEGREE)) {
            return AngleUnit.DEGREES;
        }
        if (source.contains(RADIAN + " ") || source.endsWith(RADIAN)) {
            return AngleUnit.RADIANS;
        }
        return null;
    }

    /**
     * Formats and converts provided angle value and unit using metric system. This implementation ignores provided
     * unit system.
     *
     * @param value  a measurement value.
     * @param unit   a measurement unit.
     * @param system ignored.
     * @return a string representation of angle value and unit.
     */
    @Override
    public String formatAndConvert(final Number value, final AngleUnit unit, final UnitSystem system) {
        return format(value, unit);
    }

    /**
     * Returns unit string representation.
     *
     * @param unit an angle unit.
     * @return its string representation.
     */
    @Override
    public String getUnitSymbol(final AngleUnit unit) {
        if (unit == AngleUnit.DEGREES) {
            return DEGREE;
        } else {
            return RADIAN;
        }
    }

    /**
     * Formats provided angle into degrees and decimal minutes.
     *
     * @param angle angle to be formatted.
     * @return string representation of provided angle expressed as degrees and decimal minutes.
     */
    public String formatDegreesAndMinutes(final Angle angle) {
        final var degreesAndMinutes = AngleConverter.toDegreesAndMinutes(angle);
        return MessageFormat.format(DEGREES_AND_MINUTES_MESSAGE_PATTERN,
                numberFormat.format(degreesAndMinutes[0]),
                numberFormat.format(degreesAndMinutes[1]));
    }

    /**
     * Formats provided angle into degrees, minutes and decimal seconds.
     *
     * @param angle angle to be formatted.
     * @return string representation of provided angle expressed as degrees, minutes and decimal seconds.
     */
    public String formatDegreesMinutesAndSeconds(final Angle angle) {
        final var degreesMinutesAndSeconds = AngleConverter.toDegreesMinutesAndSeconds(angle);
        return MessageFormat.format(DEGREES_MINUTES_AND_SECONDS_MESSAGE_PATTERN,
                numberFormat.format(degreesMinutesAndSeconds[0]),
                numberFormat.format(degreesMinutesAndSeconds[1]),
                numberFormat.format(degreesMinutesAndSeconds[2]));
    }

    /**
     * Parses provided string representation using degrees and decimal minutes format. Note: decimals are not
     * accepted on degrees value.
     *
     * @param source string to be parsed.
     * @return parsed angle.
     * @throws ParseException       if parsing fails.
     * @throws UnknownUnitException if format is not recognized.
     */
    @SuppressWarnings("Duplicates")
    public Angle parseDegreesAndMinutes(final CharSequence source) throws ParseException, UnknownUnitException {
        if (degreesAndMinutesPattern == null) {
            degreesAndMinutesPattern = Pattern.compile(DEGREES_AND_MINUTES_PATTERN);
        }

        final var matcher = degreesAndMinutesPattern.matcher(source);
        if (!matcher.matches()) {
            throw new UnknownUnitException();
        }

        final var degreeString = matcher.group(1);
        final var minuteString = matcher.group(3);

        final var degree = numberFormat.parse(degreeString);
        final var minute = numberFormat.parse(minuteString);

        final var angle = AngleConverter.fromDegreesAndMinutes(
                degree.intValue(), minute.doubleValue(), AngleUnit.DEGREES);
        return new Angle(angle, AngleUnit.DEGREES);
    }

    /**
     * Parses provided string representation using degrees, minutes and decimal seconds format.
     *
     * @param source string to be parsed.
     * @return parsed angle.
     * @throws ParseException       if parsing fails.
     * @throws UnknownUnitException if format is not recognized.
     */
    @SuppressWarnings("Duplicates")
    public Angle parseDegreesMinutesAndSeconds(final CharSequence source) throws ParseException, UnknownUnitException {
        if (degreesMinutesAndSecondsPattern == null) {
            degreesMinutesAndSecondsPattern = Pattern.compile(DEGREES_MINUTES_AND_SECONDS_PATTERN);
        }

        final var matcher = degreesMinutesAndSecondsPattern.matcher(source);
        if (!matcher.matches()) {
            throw new UnknownUnitException();
        }

        final var degreeString = matcher.group(1);
        final var minuteString = matcher.group(3);
        final var secondString = matcher.group(5);

        final var degree = numberFormat.parse(degreeString);
        final var minute = numberFormat.parse(minuteString);
        final var second = numberFormat.parse(secondString);

        final var angle = AngleConverter.fromDegreesMinutesAndSeconds(
                degree.intValue(), minute.intValue(), second.doubleValue(), AngleUnit.DEGREES);
        return new Angle(angle, AngleUnit.DEGREES);
    }
}