View Javadoc

1   /***
2    * Copyright (c) 2003, 2004, Chess iT
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without modification,
6    * are permitted provided that the following conditions are met:
7    * 
8    * - Redistributions of source code must retain the above copyright notice, this
9    *   list of conditions and the following disclaimer.
10   *
11   * - Redistributions in binary form must reproduce the above copyright notice,
12   *   this list of conditions and the following disclaimer in the documentation
13   *   and/or other materials provided with the distribution.
14   *
15   * - Neither the name of Chess iT, nor the names of its contributors may be used 
16   *   to endorse or promote products derived from this software without specific
17   *   prior written permission.
18   *
19   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
20   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
21   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
22   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 
23   * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
24   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
25   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
26   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
27   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
28   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
29   * POSSIBILITY OF SUCH DAMAGE.
30   * 
31   */
32  package nl.chess.it.util.config;
33  
34  import java.text.NumberFormat;
35  import java.text.ParsePosition;
36  import java.util.ArrayList;
37  import java.util.Iterator;
38  import java.util.List;
39  import java.util.Locale;
40  import java.util.StringTokenizer;
41  
42  
43  /***
44   * Helper class that parses a String and interprets it as a duration.
45   *
46   * @author Guus Bosman (Chess iT)
47   * @version $Revision: 1.1.1.1 $
48   */
49  class DurationParser {
50      /***
51       * Main parse method.
52       *
53       * @param key Is only used to display better error messages if necessary.
54       * @param value String to parse. Cannot be <code>null</code>.
55       *
56       * @return A duration (long) in milliseconds. Never a negative number.
57       */
58      public long getDurationInMs(String key, String value)
59                           throws ConfigurationException {
60          if (value == null) {
61              throw new NullPointerException("value cannot be null here.");
62          }
63  
64          /***
65           * Get rid of all comma's and connecting words. Makes parsing SO much easier.
66           */
67          value = value.replaceAll(",", "");
68          value = value.replaceAll("and", "");
69  
70          if (value.endsWith(".")) {
71              value = value.substring(0, value.length() - 1);
72          }
73  
74          try {
75              /***
76               * Convert the String to a List of token.
77               */
78              List tokens = parseTokens(value);
79  
80              /***
81               * Parse as numbers...
82               */
83              List realValues = convertToRealNumbers(tokens);
84  
85              /***
86               * ...and add all the numbers found to each other (taken Units into account).
87               */
88              double intermediateResult = calculateDuration(realValues);
89  
90              return (long) intermediateResult; // calculateDuration() made sure this is okay.
91          }
92          catch (InvalidPropertyException e) {
93              // fill in InvalidPropertyException with details.
94              e.setExpectedtype("duration");
95              e.setValue(value);
96              e.setPropertyname(key);
97              throw e;
98          }
99      }
100 
101     private List parseTokens(String stringValue) {
102         StringTokenizer st = new StringTokenizer(stringValue);
103         List tokens = new ArrayList();
104 
105         while (st.hasMoreTokens()) {
106             String currentToken = st.nextToken();
107             currentToken = currentToken.trim();
108 
109             //            System.out.println("Token: '" + currentToken + "'");
110             ParsePosition pp = new ParsePosition(0);
111 
112             Number number = NumberFormat.getInstance(Locale.US).parse(currentToken, pp);
113             String leftOver = currentToken.substring(pp.getIndex());
114 
115             if (number != null) {
116                 tokens.add(number);
117             }
118 
119             if ((leftOver != null) && (!leftOver.trim().equals(""))) {
120                 tokens.add(leftOver);
121             }
122         }
123 
124         return tokens;
125     }
126 
127     /***
128      * Where applicable, convert string representations to Number objects (i.e. replace "4" with new Integer(4), and "two" with new
129      * Integer(2).
130      *
131      * @param tokens List to convert. Cannot be <code>null</code>.
132      *
133      * @return New List with Numbers and Strings.
134      */
135     private List convertToRealNumbers(List tokens) {
136         List realValues = new ArrayList();
137 
138         for (Iterator iter = tokens.iterator(); iter.hasNext();) {
139             Object element = iter.next();
140 
141             if (element instanceof String) {
142                 Number currentNumberValue = getNumberValue((String) element);
143 
144                 if (currentNumberValue != null) {
145                     realValues.add(currentNumberValue);
146                 }
147                 else {
148                     realValues.add(element);
149                 }
150             }
151             else {
152                 realValues.add(element);
153             }
154         }
155 
156         return realValues;
157     }
158 
159     private double calculateDuration(List realValues) {
160         boolean expectingNumber = true; //basically a state. If <true> we want to see a number token, otherwise we want to see a unit token.
161         double finalResult = 0;
162 
163         Number currentNumberValue = null;
164         
165         if (realValues.size() == 0) {
166 			throw new PropertyParseException("Nothing to parse.");
167         }
168         	
169         for (Iterator iter = realValues.iterator(); iter.hasNext();) {
170             Object element = iter.next();
171 
172             if (element instanceof Number) {
173             	
174                 if (!expectingNumber) {
175                     throw new PropertyParseException("Unexpected token '" + element + "'. Didn't expect a number.");
176                 }
177 
178                 currentNumberValue = (Number) element;
179 
180                 expectingNumber = false;
181             }
182             else {
183                 // String, probably.
184                 if (expectingNumber) {
185                     if (currentNumberValue == null) {
186                         throw new PropertyParseException("Unexpected token '" + element + "'. Expected a number.");
187                     }
188                 }
189 
190                 if (currentNumberValue instanceof Double) {
191                     if (((Double) currentNumberValue).isInfinite()) {
192                         throw new PropertyParseException("Overflow");
193                     }
194                 }
195 
196                 double currentDoubleValue = currentNumberValue.doubleValue();
197 
198                 if (currentDoubleValue < 0) {
199                     throw new PropertyParseException("Negative number '" + currentDoubleValue + "' not allowed.");
200                 }
201                 
202                 long currentLongValue = currentNumberValue.longValue();
203 
204                 if (currentLongValue < currentDoubleValue - 1) {
205                     throw new PropertyParseException("Overflow");
206                 }
207 
208                 long multiplier = getMultiplierForTimeString((String) element, (currentLongValue == 1));
209                 double intermediateResult = multiplier * currentDoubleValue;
210 
211                 finalResult += intermediateResult;
212 
213                 long finalResultTest = (long) finalResult;
214 
215                 if (finalResultTest < finalResult - 1) {
216                     throw new PropertyParseException("Overflow");
217                 }
218 
219                 // next one must be a number again
220                 expectingNumber = true;
221                 currentNumberValue = null;
222             }
223         }
224 
225         if (!expectingNumber) {
226             // we're still expected an unit to be read. Not good.
227             throw new PropertyParseException("Expected a unit but reached end of input.");
228         }
229 
230         return finalResult;
231     }
232 
233     private Number getNumberValue(String string) {
234         string = string.toLowerCase();
235 
236         if (string.equals("zero")) {
237             return Integer.valueOf("0");
238         }
239 
240         if (string.equals("a")) {
241             return Integer.valueOf("1");
242         }
243 
244         if (string.equals("one")) {
245             return Integer.valueOf("1");
246         }
247 
248         if (string.equals("two")) {
249             return Integer.valueOf("2");
250         }
251 
252         if (string.equals("three")) {
253             return Integer.valueOf("3");
254         }
255 
256         if (string.equals("four")) {
257             return Integer.valueOf("4");
258         }
259 
260         if (string.equals("five")) {
261             return Integer.valueOf("5");
262         }
263 
264         if (string.equals("six")) {
265             return Integer.valueOf("6");
266         }
267 
268         if (string.equals("seven")) {
269             return Integer.valueOf("7");
270         }
271 
272         if (string.equals("eight")) {
273             return Integer.valueOf("8");
274         }
275 
276         if (string.equals("nine")) {
277             return Integer.valueOf("9");
278         }
279 
280         if (string.equals("ten")) {
281             return Integer.valueOf("10");
282         }
283 
284         return null;
285     }
286 
287     private long getMultiplierForTimeString(String string, boolean singleWord) {
288         string = string.toLowerCase();
289 
290         boolean addS = !singleWord;
291 
292         if (string.equals("week" + (addS ? "s" : ""))) {
293             return 1000 * 60 * 60 * 24 * 7;
294         }
295 
296         if (string.equals("day" + (addS ? "s" : ""))) {
297             return 1000 * 60 * 60 * 24;
298         }
299 
300         if (string.equals("hour" + (addS ? "s" : ""))) {
301             return 60 * 60 * 1000;
302         }
303 
304         if (string.equals("minute" + (addS ? "s" : ""))) {
305             return 60 * 1000;
306         }
307 
308         if (string.equals("second" + (addS ? "s" : ""))) {
309             return 1000;
310         }
311 
312         if (string.equals("ms")) {
313             return 1;
314         }
315 
316         if (string.equals("s")) {
317             return 1000;
318         }
319 
320         throw new PropertyParseException("Unknown unit: '" + string + "'");
321     }
322 }