HMS.java 10 KB
/*
 * $Id: HMS.java,v 1.4 2009/04/21 13:31:17 abrighto Exp $
 */

package jsky.coords;

import jsky.util.StringUtil;

import java.io.Serializable;
import java.text.NumberFormat;
import java.util.Locale;
import java.util.StringTokenizer;

/**
 * Class representing a value of the form "hours:min:sec".
 *
 * @author Allan Brighton
 * @version $Revision: 1.4 $
 */
public class HMS implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * On the handling of -0: from the javadoc for Double.equals():
     * "If d1 represents +0.0 while d2 represents -0.0, or vice versa,
     * the equal test has the value false, even though +0.0==-0.0 has the
     * value true."
     * The test for 0.0 != -0.0 only works with Double.equals(minusZero).
     * This case shows up in HMS values with zero hours and negative values,
     * such as "-00 24 32"
     */
    private static final Double MINUS_ZERO = -0.0;

    // Number formats for 2 digit hours and minutes
    private static final NumberFormat NF = NumberFormat.getInstance(Locale.US);

    // Number formats for seconds
    private static final NumberFormat NF_SEC = NumberFormat.getInstance(Locale.US);

    static {
        NF.setMinimumIntegerDigits(2);
        NF.setMaximumIntegerDigits(2);
        NF.setMaximumFractionDigits(0);

        NF_SEC.setMinimumIntegerDigits(2);
        NF_SEC.setMaximumIntegerDigits(2);
        NF_SEC.setMinimumFractionDigits(3);
        NF_SEC.setMaximumFractionDigits(3);
    }

    /**
     * number of hours
     */
    private int hours;

    /**
     * number of minutes
     */
    private int min;

    /**
     * number of seconds
     */
    private double sec;

    /**
     * value converted to decimal
     */
    private double val;

    /**
     * set to 1 or -1
     */
    private byte sign = 1;

    /* true if value has been initialized */
    private boolean initialized = false;


    /**
     * Default constructor: initialize to null values
     */
    public HMS() {
    }

    /**
     * Initialize with the given hours, minutes and seconds.
     */
    public HMS(double hours, int min, double sec) {
        set(hours, min, sec);
    }

    /**
     * Initialize from a decimal hours value and calculate H:M:S.sss.
     */
    public HMS(double val) {
        setVal(val);
    }

    /**
     * Copy constructor
     */
    public HMS(HMS hms) {
        setVal(hms.val);
    }

    /**
     * Initialize from a string value, in format H:M:S.sss, hh, or H M
     * S.  If the value is not in H:M:S and is not an integer (has a
     * decimal point), assume the value is in deg convert to hours by
     * dividing by 15. (Reason: some catalog servers returns RA in h:m:s
     * while others return it in decimal deg.)
     */
    public HMS(String s) {
        this(s, false);
    }

    /**
     * Initialize from a string value, in format H:M:S.sss, hh, or
     * H M S.  If the value is not in H:M:S and is not an
     * integer (has a decimal point), and hflag is true,
     * assume the value is in deg and convert to hours by dividing by 15.
     *
     * @param s the RA string
     * @param hflag if true, assume RA is always in hours, otherwise, if it has a decimal point,
     * assume deg
     */
    public HMS(String s, boolean hflag) {
        s = StringUtil.replace(s, ",", "."); // Treat ',' like '.', by request
        double[] vals = {0.0, 0.0, 0.0};
        StringTokenizer tok = new StringTokenizer(s, ": ");
        int n = 0;
        while (n < 3 && tok.hasMoreTokens()) {
            vals[n++] = Double.valueOf(tok.nextToken());
        }

        if (n >= 2) {
            set(vals[0], (int) vals[1], vals[2]);
        } else if (n == 1) {
            if (!hflag && s.indexOf('.') != -1) {
                setVal(vals[0] / 15.);
            } else {
                setVal(vals[0]);
            }
        } else {
            throw new RuntimeException("Expected a string of the form hh:mm:ss.sss, but got: '" + s + "'");
        }
    }

    /**
     * Set the hours, minutes and seconds.
     */
    public void set(double hours, int min, double sec) {
        this.hours = (int) hours;
        this.min = min;
        this.sec = sec;

        val = (sec / 60.0 + min) / 60.0;

        if (hours < 0.0 || new Double(hours).equals(MINUS_ZERO)) {
            val = hours - val;
            this.hours = -this.hours;
            sign = -1;
        } else {
            val = this.hours + val;
            sign = 1;
        }
        initialized = true;
    }

    /**
     * Set from a decimal value (hours) and calculate H:M:S.sss.
     */
    public void setVal(double val) {
        this.val = val;

        double v = val; // check also for neg zero
        if (v < 0.0 || new Double(v).equals(MINUS_ZERO)) {
            sign = -1;
            v = -v;
        } else {
            sign = 1;
        }

        double dd = v + 0.0000000001;
        hours = (int) dd;
        double md = (dd - hours) * 60.;
        min = (int) md;
        sec = (md - min) * 60.;
        initialized = true;
    }

    /**
     * Return the value as a String in the form hh:mm:ss.sss.
     * Seconds are formatted with leading zero if needed.
     * The seconds are formatted with 3 digits of precision.
     */
    public String toString() {

        // paulbalm6 - Feb 2011: If sec is 59.9999, it will be formatted here as
        // 60.0, e.g. +00:59:60.
        // If this happens, we need to roll the minutes (and possibly the hours)
        // to the next value. The correct representation of the above is
        // +01:00:00 (and not +00:60:00!).
        String secStr = NF_SEC.format(sec);
        int min_copy = min;
        int hours_copy = hours;

        // Did the seconds format to 60 seconds?
        if (NF_SEC.format(60).equals(secStr)) {
            secStr = NF_SEC.format(0);
            // Roll minute, possibly hour.
            // Note that the roll here is independent of the sign (always +1).
            min_copy++;
            if (min_copy == 60) {
                min_copy = 0;
                hours_copy++;
            }
        }

        // sign
        String signStr;
        if (sign == -1) {
            signStr = "-";
        } else {
            signStr = "";
        }

        return signStr
                + NF.format(hours_copy)
                + ":"
                + NF.format(min_copy)
                + ":"
                + secStr;
    }

    /**
     * Return the value as a String in the form hh:mm:ss.sss,
     * or if showSeconds is false, hh:mm.
     * <p>
     * The seconds are simply dropped. No rounding is performed.
     */
    public String toString(boolean showSeconds) {
        if (showSeconds) {
            return toString();
        }

        // sign
        String signStr;
        if (sign == -1) {
            signStr = "-";
        } else {
            signStr = " ";
        }

        return signStr
                + NF.format(hours)
                + ":"
                + NF.format(min);
    }

    /**
     * Return true if this object has been initialized with a valid value
     */
    public boolean isInitialized() {
        return initialized;
    }

    /**
     * Return the number of hours (not including minutes or seconds)
     */
    public int getHours() {
        return hours;
    }

    /**
     * Return the number of minutes (not including hours or seconds)
     */
    public int getMin() {
        return min;
    }

    /**
     * Return the number of seconds (not including hours and minutes)
     */
    public double getSec() {
        return sec;
    }

    /**
     * Return the value (fractional number of hours) as a double
     */
    public double getVal() {
        return val;
    }

    /**
     * Return the sign of the value
     */
    public byte getSign() {
        return sign;
    }

    /**
     * Define equality based on the value
     */
    public boolean equals(Object obj) {
        return (obj instanceof HMS && val == ((HMS) obj).val);
    }


    /**
     * Test cases
     */
    public static void main(String[] args) {

        HMS h = new HMS(3, 19, 48.23);
        System.out.println("HMS(3, 19, 48.23) == " + h + " == " + h.getVal());

        if (!(h.equals(new HMS(h.getVal())))) {
            System.out.println("Equality test failed: " + h + " != " + new HMS(h.getVal()));
        }

        h = new HMS(41, 30, 42.2);
        System.out.println("41 30 42.2 = " + h + " = " + h.getVal() + " = " + new HMS(h.getVal()));

        h = new HMS(-41, 30, 2.2);
        System.out.println("-41 30 2.2 = " + h + " = " + h.getVal() + " = " + new HMS(h.getVal()));

        h = new HMS("-41 30 42.2");
        System.out.println("-41 30 42.2 = " + h + " = " + h.getVal() + " = " + new HMS(h.getVal()));

        h = new HMS("1:01:02.34567");
        System.out.println("1:01:02.34567 = " + h + " = " + h.getVal() + " = " + new HMS(h.getVal()));

        h = new HMS("1:01:02.34567");
        System.out.println("1:01:02.34567 = " + h + " = " + h.getVal() + " = " + new HMS(h.getVal()));

        h = new HMS(-0., 15, 33.3333);
        System.out.println("-0 15 33.3333 = " + h + " = " + h.getVal() + " = " + new HMS(h.getVal()));

        h = new HMS(-0.0001);
        System.out.println("-0.0001 = " + h + " = " + h.getVal() + " = " + new HMS(h.getVal()));

        h = new HMS(121.39583332 / 15.);
        System.out.println("121.39583332/15. = " + h + " = " + h.getVal() + " = " + new HMS(h.getVal()));

        h = new HMS(121.09583332 / 15.);
        System.out.println("121.09583332/15. = " + h + " = " + h.getVal() + " = " + new HMS(h.getVal()));

        h = new HMS(-121.39583332 / 15.);
        System.out.println("-121.39583332/15. = " + h + " = " + h.getVal() + " = " + new HMS(h.getVal()));

        h = new HMS(-121.09583332 / 15.);
        System.out.println("-121.09583332/15. = " + h + " = " + h.getVal() + " = " + new HMS(h.getVal()));
        
        // paulbalm6 - Feb 2010: Check rounding + roll to next minute or hour
        double x = 5.4999999809265145 / 15;
        h = new HMS(x);
        System.out.println(x + " deg  = " + h);
        
        x = 5.9999999809265145;
        h = new HMS(x);
        System.out.println(x + " deg  = " + h);
    }
}