001    package edu.nrao.sss.math;
002    
003    import java.math.BigDecimal;
004    
005    /**
006     * An interval on the number line from one number to another.
007     * Whether or not the endpoints are part of an interval is
008     * determined by its {@link IntervalType type}.
009     * <p>
010     * <b>Version Info:</b>
011     * <table style="margin-left:2em">
012     *   <tr><td>$Revision: 567 $</td></tr>
013     *   <tr><td>$Date: 2007-04-26 16:35:46 -0600 (Thu, 26 Apr 2007) $</td></tr>
014     *   <tr><td>$Author: dharland $</td></tr>
015     * </table></p>
016     * 
017     * @author David M. Harland
018     * @since 2007-04-25
019     */
020    public class NumberInterval
021    {
022      private IntervalType intervalType;
023      private BigDecimal   low, high;
024      
025      /**
026       * Creates a new interval with the given properties.
027       * 
028       * @param type determines which endpoints, if either, are part of this
029       *             interval.
030       * @param a one of the endpoints.
031       * @param b the other endpoint.
032       */
033      public NumberInterval(IntervalType type, Number a, Number b)
034      {
035        set(a,b);
036        set(type);
037      }
038      
039      /** Used by constructor & public set method. */
040      private void set(Number a, Number b)
041      {
042        BigDecimal bdA = numberToBigDecimal(a);
043        BigDecimal bdB = numberToBigDecimal(b);
044        
045        if (bdA.compareTo(bdB) <= 0)
046        {
047          low  = bdA;
048          high = bdB;
049        }
050        else
051        {
052          low  = bdB;
053          high = bdA;
054        }
055      }
056      
057      /** Used by constructor & public set method. */
058      private void set(IntervalType type)
059      {
060        intervalType = (type == null) ? IntervalType.LOW_CLOSED_HIGH_OPEN : type;
061      }
062      
063      /** Converts a Number to a BigDecimal. */
064      private BigDecimal numberToBigDecimal(Number n)
065      {
066        BigDecimal bd = null;
067        
068        try
069        {
070          bd = new BigDecimal(n.toString());
071        }
072        catch (NumberFormatException ex)
073        {
074          //TODO BigD does not handle NaN or infinity.
075          //     Might be nice to handle infinity.
076          bd = new BigDecimal(Double.toString(n.doubleValue()));
077        }
078        
079        return bd;
080      }
081      
082      /**
083       * Sets the endpoints of this interval.
084       * Clients do not need to present the endpoints in any particular order.
085       * This method will compare the two endpoints to determine which
086       * is higher and which is lower.
087       * 
088       * @param a one of the endpoints.
089       * @param b the other endpoint.
090       */
091      public void setInterval(Number a, Number b)  { set(a,b); }
092      
093      /**
094       * Sets the value that determines which endpoints, if either, are in
095       * this interval.
096       * @param newType the type of this interval (open, closed, etc.).
097       */
098      public void setType(IntervalType newType)  { set(newType); }
099      
100      /**
101       * Returns <i>true</i> if this interval contains {@code candidate}.
102       * @param candidate a number that might be contained in this interval.
103       * @return <i>true</i> if this interval contains {@code candidate}.
104       */
105      public boolean contains(Number candidate)
106      {
107        BigDecimal param = numberToBigDecimal(candidate);
108        
109        //Though we usually avoid multiple returns in a method, the process
110        //of elimination leads to easier-to-read conditionals.
111    
112        int paramCompareToLow  = param.compareTo(low);
113        int paramCompareToHigh = param.compareTo(high);
114        
115        //param is clearly in the range
116        if ((paramCompareToLow > 0) && (paramCompareToHigh < 0))
117          return true;
118        
119        //param equals low end of range, and low end is part of interval
120        if ((paramCompareToLow == 0) &&
121            (intervalType.equals(IntervalType.CLOSED)) ||
122             intervalType.equals(IntervalType.LOW_CLOSED_HIGH_OPEN))
123          return true;
124        
125        //param equals high end of range, and high end is part of interval
126        if ((paramCompareToHigh == 0) &&
127            (intervalType.equals(IntervalType.CLOSED)) ||
128             intervalType.equals(IntervalType.LOW_OPEN_HIGH_CLOSED))
129          return true;
130        
131        //param is either outside the range or equal to an endpoint that
132        //is not part of the range
133        return false;
134      }
135      
136      /**
137       * Returns a string of form <tt>[low..high)</tt>, where a square bracket
138       * indicates that the interval is closed on that end and a rounded bracket
139       * that it is open.
140       */
141      @Override
142      public String toString()
143      {
144        char startChar = 
145          (intervalType.equals(IntervalType.CLOSED) ||
146           intervalType.equals(IntervalType.LOW_CLOSED_HIGH_OPEN)) ? '[' : '(';
147    
148        char endChar = 
149          (intervalType.equals(IntervalType.CLOSED) ||
150           intervalType.equals(IntervalType.LOW_OPEN_HIGH_CLOSED)) ? ']' : ')';
151        
152        StringBuilder buff = new StringBuilder();
153        
154        buff.append(startChar).append(low.toString());
155        buff.append("..");
156        buff.append(high.toString()).append(endChar);
157        
158        return buff.toString();
159      }
160      
161      /**
162       * Indicates whether or not the endpoints are inside or outside an interval.
163       */
164      public static enum IntervalType
165      {
166        /**
167         * Neither endpoint is contained in the interval. 
168         */
169        OPEN,
170        
171        /**
172         * Both endpoints are contained in the interval.
173         */
174        CLOSED,
175        
176        /**
177         * The low endpoint is in the interval; the high endpoint is not.
178         * This is the usual definition of a half-open interval.
179         */
180        LOW_CLOSED_HIGH_OPEN,
181        
182        /**
183         * The low endpoint is not in the interval; the high endpoint is.
184         */
185        LOW_OPEN_HIGH_CLOSED;
186      }
187    }