001    package edu.nrao.sss.math;
002    
003    import static edu.nrao.sss.math.NumberSet.LookupMethod;
004    
005    import java.math.BigDecimal;
006    import java.util.Set;
007    import java.util.SortedSet;
008    import java.util.TreeSet;
009    
010    
011    /**
012     * Creator of number sets.
013     * Clients may fetch a factory by using {@link NumberSet#factory}.
014     * <p>
015     * <b>Version Info:</b>
016     * <table style="margin-left:2em">
017     *   <tr><td>$Revision: 1707 $</td></tr>
018     *   <tr><td>$Date: 2008-11-14 10:23:59 -0700 (Fri, 14 Nov 2008) $</td></tr>
019     *   <tr><td>$Author: dharland $</td></tr>
020     * </table></p>
021     * 
022     * @author David M. Harland
023     * @since 2007-03-01
024     */
025    public class NumberSetFactory
026    {
027      /** The single instance of this factory. */
028      static final NumberSetFactory instance = new NumberSetFactory();
029    
030      /** Enforce singleton pattern. */
031      private NumberSetFactory()  { }
032      
033      /**
034       * Creates a continuous set of numbers.
035       * 
036       * @param minValue the smallest value in this set.
037       * @param maxValue the largest value in this set.
038       * @param lookupMethod the manner in which a value is found in this set.
039       * 
040       * @return a new continuous set of numbers.
041       */
042      public NumberSet makeContinuousSet(Number minValue,
043                                         Number maxValue, LookupMethod lookupMethod)
044      {
045        assertValidMinMax(minValue, maxValue, false);
046        
047        lookupMethod = makeValidLookupMethod(lookupMethod);
048        
049        return new ContinuousNumberSet(minValue, maxValue, lookupMethod);
050      }
051      
052      /**
053       * Creates a discrete set of numbers.
054       * 
055       * @param discreteValues the numbers in this set.
056       * @param lookupMethod the manner in which a value is found in this set.
057       * 
058       * @return a new discrete set of numbers.
059       */
060      public NumberSet makeDiscreteSet(Set<? extends Number>  discreteValues,
061                                       LookupMethod lookupMethod)
062      {
063        if (discreteValues == null)
064          discreteValues = new TreeSet<Number>();
065        
066        lookupMethod = makeValidLookupMethod(lookupMethod);
067        
068        return new DiscreteNumberSet(discreteValues, lookupMethod);
069      }
070    
071      /**
072       * Creates a discrete set of numbers.
073       * <p>
074       * The numeric parameters are used to construct the values held in the
075       * returned set.
076       * The smallest such value is <tt>minValue</tt>.
077       * The next smallest value is <tt>minValue</tt> plus <tt>stepSize</tt>,
078       * and so on until the largest value, which will be less than or equal
079       * to <tt>maxValue</tt>, is added.</p> 
080       * 
081       * @param minValue the smallest value in this set.
082       * @param maxValue the largest possible value in this set.  Note that the
083       *                 largest value <i>actually</i> held in this set could be
084       *                 smaller.  This happens when the distance from
085       *                 <tt>minValue</tt> to <tt>maxValue</tt> is not an
086       *                 integral multiple of <tt>stepSize</tt>.
087       * @param stepSize an increment used to create other values for this set.
088       * @param lookupMethod the manner in which a value is found in this set.
089       * 
090       * @return a new discrete set of numbers.
091       */
092      public NumberSet makeDiscreteSetAdditive(Number minValue,
093                                               Number maxValue,
094                                               Number stepSize,
095                                               LookupMethod lookupMethod)
096      {
097        assertValidMinMax(minValue, maxValue, true);
098        assertValidStepSize(stepSize);
099        
100        lookupMethod = makeValidLookupMethod(lookupMethod);
101        
102        TreeSet<BigDecimal> values = new TreeSet<BigDecimal>();
103        
104        BigDecimal currVal = new BigDecimal(minValue.toString());
105        BigDecimal maxVal  = new BigDecimal(maxValue.toString());
106        BigDecimal stepVal = new BigDecimal(stepSize.toString());
107        
108        while (currVal.compareTo(maxVal) <= 0)
109        {
110          values.add(currVal);
111          
112          currVal = currVal.add(stepVal);
113        }
114        
115        return new DiscreteNumberSet(values, lookupMethod);
116      }
117    
118      /**
119       * Creates a discrete set of numbers.
120       * <p>
121       * The numeric parameters are used to construct the values held in the
122       * returned set.
123       * The smallest such value is <tt>minValue</tt>.
124       * The next smallest value is <tt>minValue</tt> multiplied by
125       * <tt>stepSize</tt>, and so on until the largest value, which will be less
126       * than or equal to <tt>maxValue</tt>, is added.</p> 
127       * 
128       * @param minValue the smallest value in this set.
129       * @param maxValue the largest possible value in this set.  Note that the
130       *                 largest value <i>actually</i> held in this set could be
131       *                 smaller.
132       * @param multiplier a value used to create other values for this set.
133       * @param lookupMethod the manner in which a value is found in this set.
134       * 
135       * @return a new discrete set of numbers.
136       */
137      public NumberSet makeDiscreteSetMultiplicative(Number minValue,
138                                                     Number maxValue,
139                                                     Number multiplier,
140                                                     LookupMethod lookupMethod)
141      {
142        if (minValue.doubleValue() == 0.0)
143          throw new IllegalArgumentException("minValue may not be zero");
144    
145        assertValidMinMax(minValue, maxValue, true);
146        assertValidMultiplier(multiplier);
147        
148        lookupMethod = makeValidLookupMethod(lookupMethod);
149        
150        TreeSet<BigDecimal> values = new TreeSet<BigDecimal>();
151        
152        BigDecimal currVal = new BigDecimal(minValue.toString());
153        BigDecimal maxVal  = new BigDecimal(maxValue.toString());
154        BigDecimal multVal = new BigDecimal(multiplier.toString());
155        
156        while (currVal.compareTo(maxVal) <= 0)
157        {
158          values.add(currVal);
159          
160          currVal = currVal.multiply(multVal);
161        }
162        
163        return new DiscreteNumberSet(values, lookupMethod);
164      }
165      
166      /** Ensures valid minimum and maximum values. */
167      private void assertValidMinMax(Number minValue,
168                                     Number maxValue, boolean discrete)
169      {
170        if (minValue.doubleValue() > maxValue.doubleValue())
171          throw new IllegalArgumentException("May not have minValue > maxValue");
172    
173        if (Double.isNaN(minValue.doubleValue()))
174          throw new IllegalArgumentException("minValue may not be NaN");
175    
176        if (Double.isNaN(maxValue.doubleValue()))
177          throw new IllegalArgumentException("maxValue may not be NaN");
178        
179        if (discrete)
180        {
181          if (Double.isInfinite(minValue.doubleValue()))
182            throw new IllegalArgumentException("minValue may not be infinite");
183        
184          if (Double.isInfinite(maxValue.doubleValue()))
185            throw new IllegalArgumentException("maxValue may not be infinite");
186        }
187      }
188      
189      private void assertValidStepSize(Number stepSize)
190      {
191        if (stepSize.doubleValue() <= 0.0)
192          throw new IllegalArgumentException("stepSize must be greater than zero");
193      }
194      
195      private void assertValidMultiplier(Number multiplier)
196      {
197        if (multiplier.doubleValue() <= 1.0)
198          throw new IllegalArgumentException("multiplier must be greater than one");
199      }
200      
201      private LookupMethod makeValidLookupMethod(LookupMethod method)
202      {
203        return method == null ? LookupMethod.EQUAL : method;
204      }
205      
206      //============================================================================
207      //
208      //============================================================================
209    
210      /** A discrete set of numbers. */
211      private class DiscreteNumberSet implements NumberSet
212      {
213        private LookupMethod          method;
214        private SortedSet<BigDecimal> sortedValues;
215        
216        DiscreteNumberSet(Set<? extends Number> values, LookupMethod lookupMethod)
217        {
218          method       = lookupMethod;
219          sortedValues = new TreeSet<BigDecimal>();
220          
221          for (Number value : values)
222            sortedValues.add(new BigDecimal(value.toString()));
223        }
224        
225        public DiscreteNumberSet(SortedSet<BigDecimal> values,
226                                 LookupMethod          lookupMethod)
227        {
228          method       = lookupMethod;
229          sortedValues = values;
230        }
231        
232        public Number getBestMatchFor(Number myNumber)
233        {
234          Number answer;
235          
236          BigDecimal myVal = new BigDecimal(myNumber.toString());
237          
238          switch (method)
239          {
240            case EQUAL:
241              answer = sortedValues.contains(myVal) ? myVal : null;
242              break;
243              
244            case LARGEST_LESS_THAN_OR_EQUAL:
245              answer = findLteMatch(myNumber);
246              break;
247              
248            case SMALLEST_GREATER_THAN_OR_EQUAL:
249              answer = findGteMatch(myNumber);
250              break;
251              
252            default:
253              answer = null;
254              throw new
255                RuntimeException("PROGRAMMER ERROR: Unrecognized LookupMethod of " +
256                                 method.toString());
257          }
258          
259          return answer;
260        }
261        
262        private Number findLteMatch(Number myNumber)
263        {
264          //Initialize match to last value in case we run off top of set
265          Number match     = sortedValues.last();
266          Number prevValue = null;
267          
268          //The Number interface does not implement Comparable<Number>, so
269          //use BigDecimal.
270          BigDecimal myValue = new BigDecimal(myNumber.toString());
271          
272          for (BigDecimal value : sortedValues)
273          {
274            int comparison = myValue.compareTo(value);
275            
276            if (comparison <= 0)
277            {
278              match = (comparison == 0) ? value : prevValue;
279              break;
280            }
281            
282            prevValue = value;
283          }
284          
285          return match;
286        }
287        
288        private Number findGteMatch(Number myNumber)
289        {
290          //Reverse the sort so that greatest number is 1st
291          int valueCount = sortedValues.size();
292          BigDecimal[] values = new BigDecimal[valueCount];
293          
294          int v = valueCount;
295          
296          for (BigDecimal value : sortedValues)
297            values[--v] = value;
298          
299          //Initialize match to last value in case we run off top of set
300          Number match     = values[valueCount-1];
301          Number prevValue = null;
302          
303          //The Number interface does not implement Comparable<Number>, so
304          //use BigDecimal.
305          BigDecimal myValue = new BigDecimal(myNumber.toString());
306    
307          for (BigDecimal value : values)
308          {
309            int comparison = myValue.compareTo(value);
310            
311            if (comparison >= 0)
312            {
313              match = (comparison == 0) ? value : prevValue;
314              break;
315            }
316            
317            prevValue = value;
318          }
319          
320          return match;
321        }
322      }
323      
324      //============================================================================
325      //
326      //============================================================================
327    
328      /** A continuous set of numbers. */
329      private class ContinuousNumberSet implements NumberSet
330      {
331        private LookupMethod method;
332        private BigDecimal   minVal, maxVal;
333        
334        ContinuousNumberSet(Number minValue,
335                            Number maxValue, LookupMethod lookupMethod)
336        {
337          minVal = new BigDecimal(minValue.toString());
338          maxVal = new BigDecimal(maxValue.toString());
339          
340          method = lookupMethod;
341        }
342        
343        public Number getBestMatchFor(Number myNumber)
344        {
345          Number answer;
346          
347          BigDecimal myVal = new BigDecimal(myNumber.toString());
348    
349          //If number is greater than max, return max or null
350          if (myVal.compareTo(maxVal) > 0)
351          {
352            if (method.equals(LookupMethod.LARGEST_LESS_THAN_OR_EQUAL))
353              answer = maxVal;
354            else
355              answer = null;
356          }
357          //Else if number is less than min, return min or null
358          else if (myVal.compareTo(minVal) < 0)
359          {
360            if (method.equals(LookupMethod.SMALLEST_GREATER_THAN_OR_EQUAL))
361              answer = minVal;
362            else
363              answer = null;
364          }
365          //Otherwise, return their number
366          else
367          {
368            answer = myNumber;
369          }
370          
371          return answer;
372        }
373      }
374    }