001 /*
002 * Copyright (c) 2005 Einar Pehrson <einar@pehrson.nu>.
003 *
004 * This file is part of
005 * CleanSheets - a spreadsheet application for the Java platform.
006 *
007 * CleanSheets is free software; you can redistribute it and/or modify
008 * it under the terms of the GNU General Public License as published by
009 * the Free Software Foundation; either version 2 of the License, or
010 * (at your option) any later version.
011 *
012 * CleanSheets is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
015 * GNU General Public License for more details.
016 *
017 * You should have received a copy of the GNU General Public License
018 * along with CleanSheets; if not, write to the Free Software
019 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
020 */
021 package csheets.core;
022
023 import java.io.Serializable;
024 import java.text.DateFormat;
025 import java.text.Format;
026 import java.text.NumberFormat;
027 import java.text.ParseException;
028 import java.text.ParsePosition;
029 import java.util.Arrays;
030 import java.util.Calendar;
031 import java.util.Date;
032 import java.util.GregorianCalendar;
033
034 /**
035 * A typed value that a cell can contain.
036 * @author Einar Pehrson
037 */
038 public class Value implements Comparable<Value>, Serializable {
039
040 /** The unique version identifier used for serialization */
041 private static final long serialVersionUID = 7140236908025236588L;
042
043 /** The recognized types of values */
044 public enum Type {
045
046 /** Denotes a value of undefined type */
047 UNDEFINED,
048
049 /** Denotes a numeric value, with or without decimals */
050 NUMERIC,
051
052 /** Denotes a text value, or a type of value derived from text */
053 TEXT,
054
055 /** Denotes a boolean value, i.e. true or false */
056 BOOLEAN,
057
058 /** Denotes a date, time or date/time value */
059 DATE,
060
061 /** Denotes a row vector, column vector or two-dimensional matrix of values */
062 MATRIX,
063
064 /** Denotes an error, e.g. a type mismatch */
065 ERROR
066 }
067
068 /** The value */
069 private Serializable value;
070
071 /** The type of the value */
072 private Type type = Type.UNDEFINED;
073
074 /**
075 * Creates a null value.
076 */
077 public Value() {}
078
079 /**
080 * Creates a numeric value.
081 * @param number the number of the value
082 */
083 public Value(Number number) {
084 this.type = Type.NUMERIC;
085 if ((number instanceof Float || number instanceof Double)
086 && number.doubleValue() == number.longValue())
087 this.value = number.longValue();
088 else
089 this.value = number;
090 }
091
092 /**
093 * Creates a text value.
094 * @param text the text of the value
095 */
096 public Value(String text) {
097 this.type = Type.TEXT;
098 this.value = text;
099 }
100
101 /**
102 * Creates a boolean value.
103 * @param booleanValue the boolean of the value
104 */
105 public Value(Boolean booleanValue) {
106 this.type = Type.BOOLEAN;
107 this.value = booleanValue;
108 }
109
110 /**
111 * Creates a date value.
112 * @param date the date of the value
113 */
114 public Value(Date date) {
115 this.type = Type.DATE;
116 this.value = date;
117 }
118
119 /**
120 * Creates a one-dimensional matrix value (vector).
121 * @param matrix the value vector
122 */
123 public Value(Value[] matrix) {
124 this(new Value[][] {matrix});
125 }
126
127 /**
128 * Creates a two-dimensional matrix value.
129 * @param matrix the value matrix
130 */
131 public Value(Value[][] matrix) {
132 this.type = Type.MATRIX;
133 this.value = matrix;
134 }
135
136 /**
137 * Creates an error value.
138 * @param error the error of the value
139 */
140 public Value(Throwable error) {
141 this.type = Type.ERROR;
142 this.value = error;
143 }
144
145 /**
146 * Returns the value in untyped form.
147 * @return the value
148 */
149 public final Object toAny() {
150 return value;
151 }
152
153 /**
154 * Returns the type of the value.
155 * @return the type of the value
156 */
157 public final Type getType() {
158 return type;
159 }
160
161 /**
162 * Returns whether the value is of the given type.
163 * @param type the type of value to check against
164 * @return whether the value is of the given type
165 */
166 public final boolean isOfType(Type type) {
167 return this.type == type;
168 }
169
170 /**
171 * Returns a numeric representation of the value.
172 * @return a numeric representation of the value
173 * @throws IllegalValueTypeException if the value cannot be converted to this type
174 */
175 public Number toNumber() throws IllegalValueTypeException {
176 if (type == Type.NUMERIC)
177 return (Number)value;
178 else
179 throw new IllegalValueTypeException(this, Type.NUMERIC);
180 }
181
182 /**
183 * Returns a primitive numeric representation of the value.
184 * @return a primitive numeric representation of the value
185 * @throws IllegalValueTypeException if the value cannot be converted to this type
186 */
187 public double toDouble() throws IllegalValueTypeException{
188 return toNumber().doubleValue();
189 }
190
191 /**
192 * Returns a text representation of the value.
193 * @return a text representation of the value
194 * @throws IllegalValueTypeException if the value cannot be converted to this type
195 */
196 public String toText() throws IllegalValueTypeException {
197 if (type == Type.TEXT)
198 return (String)value;
199 else
200 throw new IllegalValueTypeException(this, Type.TEXT);
201 }
202
203 /**
204 * Returns a boolean representation of the value.
205 * @return a boolean representation of the value
206 * @throws IllegalValueTypeException if the value cannot be converted to this type
207 */
208 public Boolean toBoolean() throws IllegalValueTypeException {
209 if (type == Type.BOOLEAN)
210 return (Boolean)value;
211 else
212 throw new IllegalValueTypeException(this, Type.BOOLEAN);
213 }
214
215 /**
216 * Returns a date representation of the value.
217 * @return a date representation of the value
218 * @throws IllegalValueTypeException if the value cannot be converted to this type
219 */
220 public Date toDate() throws IllegalValueTypeException {
221 if (type == Type.DATE)
222 return (Date)value;
223 else
224 throw new IllegalValueTypeException(this, Type.DATE);
225 }
226
227 /**
228 * Returns a matrix representation of the value.
229 * @return a matrix representation of the value
230 * @throws IllegalValueTypeException if the value cannot be converted to this type
231 */
232 public Value[][] toMatrix() throws IllegalValueTypeException {
233 if (type == Type.MATRIX)
234 return (Value[][])value;
235 else
236 throw new IllegalValueTypeException(this, Type.MATRIX);
237 }
238
239 /**
240 * Returns an error representation of the value.
241 * @return an error representation of the value
242 * @throws IllegalValueTypeException if the value cannot be converted to this type
243 */
244 public Throwable toError() throws IllegalValueTypeException {
245 if (type == Type.ERROR)
246 return (Throwable)value;
247 else
248 throw new IllegalValueTypeException(this, Type.ERROR);
249 }
250
251 /**
252 * Compares this value with the given value for order.
253 * @param otherValue the value to compare to
254 * @return a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object.
255 */
256 public int compareTo(Value otherValue) {
257 if (type == otherValue.getType())
258 try {
259 switch (type) {
260 case NUMERIC:
261 return ((Double)toDouble()).compareTo(otherValue.toDouble());
262 case TEXT:
263 return toText().compareTo(otherValue.toText());
264 case BOOLEAN:
265 return toBoolean().compareTo(otherValue.toBoolean());
266 case DATE:
267 return toDate().compareTo(otherValue.toDate());
268 case MATRIX:
269 return Arrays.hashCode((Object[])otherValue.toAny()) - Arrays.hashCode((Object[])value);
270 default:
271 return 0;
272 }
273 } catch (IllegalValueTypeException e) {
274 return -1;
275 }
276 else
277 return type.compareTo(otherValue.getType());
278 }
279
280 /**
281 * Returns whether the other object is an identical value .
282 * @param other the object to check for equality
283 * @return true if the objects are equal
284 */
285 public boolean equals(Object other) {
286 if (!(other instanceof Value) || other == null)
287 return false;
288 Value otherValue = (Value)other;
289 boolean nulls = value == null && otherValue.value == null;
290 return type == otherValue.type
291 && (nulls || (!nulls && value.equals(otherValue.value)));
292 }
293
294 /**
295 * Returns a string representation of the value.
296 * @return a string representation of the value
297 */
298 public String toString() {
299 if (value != null)
300 switch (type) {
301 case BOOLEAN:
302 return value.toString().toUpperCase();
303 case DATE:
304 return DateFormat.getDateTimeInstance(
305 DateFormat.SHORT, DateFormat.SHORT).format((Date)value);
306 case MATRIX:
307 Value[][] matrix = (Value[][])value;
308 String string = "{";
309 for (int row = 0; row < matrix.length; row++) {
310 for (int column = 0; column < matrix[row].length; column++) {
311 string += matrix[row][column];
312 if (column + 1 < matrix[row].length)
313 string += ";";
314 }
315 if (row + 1 < matrix.length)
316 string += ";\n";
317 }
318 string += "}";
319 return string;
320 default:
321 return value.toString();
322 }
323 else
324 return "";
325 }
326
327 /**
328 * Returns a string representation of the value, using the given date or
329 * number format.
330 * @param format the format to use when converting the value
331 * @return a string representation of the value
332 */
333 public String toString(Format format) {
334 if (value != null)
335 switch (type) {
336 case NUMERIC:
337 if (format instanceof NumberFormat)
338 return format.format((Number)value);
339 else
340 return value.toString();
341 case DATE:
342 if (format instanceof DateFormat)
343 return format.format((Date)value);
344 default:
345 return value.toString();
346 }
347 return "";
348 }
349
350 /**
351 * Attempts to parse a value from the given string. The value is matched
352 * against the given types in order. If no types are supplied, conversion
353 * will be attempted to boolean, date and numeric values. If no other
354 * type matches, the value will be used as a string.
355 * @param value the value
356 * @param types the types for which parsing should be attempted
357 */
358 public static Value parseValue(String value, Type... types) {
359 // Uses default types
360 if (types.length == 0)
361 types = new Type[] {Type.BOOLEAN, Type.DATE, Type.NUMERIC};
362
363 for (int i = 0; i < types.length; i++)
364 switch (types[i]) {
365 case BOOLEAN:
366 try {
367 return parseBooleanValue(value);
368 } catch (ParseException e) {}
369 break;
370
371 case DATE:
372 try {
373 return parseDateValue(value);
374 } catch (ParseException e) {}
375 break;
376
377 case NUMERIC:
378 try {
379 return parseNumericValue(value);
380 } catch (ParseException e) {}
381 break;
382 }
383
384 // Uses the string as the value
385 return new Value(value);
386 }
387
388 /**
389 * Attempts to parse a number from the given string.
390 * @param value the value
391 * @return the numeric value that was found
392 * @throws IllegalValueTypeException if no numeric value was found
393 */
394 public static Value parseNumericValue(String value) throws ParseException {
395 ParsePosition position = new ParsePosition(0);
396 Number number = NumberFormat.getInstance().parse(value, position);
397 if (position.getIndex() == value.length())
398 return new Value(number);
399 throw new ParseException(value, position.getErrorIndex());
400 }
401
402 /**
403 * Attempts to parse a boolean from the given string.
404 * @param value the value
405 * @return the boolean value that was found
406 * @throws IllegalValueTypeException if no boolean value was found
407 */
408 public static Value parseBooleanValue(String value) throws ParseException {
409 if (value.equalsIgnoreCase("true"))
410 return new Value(true);
411 else if (value.equalsIgnoreCase("false"))
412 return new Value(false);
413 else
414 throw new ParseException(value, 0);
415 }
416
417 /**
418 * Attempts to parse a date, time or date/time from the given string.
419 * @param value the value
420 * @return the date value that was found
421 * @throws IllegalValueTypeException if no date value was found
422 */
423 public static Value parseDateValue(String value) throws ParseException {
424 ParsePosition position = new ParsePosition(0);
425
426 // Attempts to parse a date or date/time
427 DateFormat[] dateFormats = new DateFormat[] {
428 DateFormat.getDateInstance(DateFormat.SHORT),
429 DateFormat.getDateInstance(DateFormat.MEDIUM),
430 DateFormat.getDateInstance(DateFormat.LONG),
431 DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT),
432 DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT),
433 DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM),
434 DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
435 };
436 for (DateFormat format : dateFormats) {
437 Date date = format.parse(value, position);
438 if (position.getIndex() == value.length())
439 return new Value(date);
440 else if (position.getIndex() > 0)
441 position.setIndex(0);
442 }
443
444 // Attempts to parse a time in the current day
445 DateFormat[] timeFormats = new DateFormat[] {
446 DateFormat.getTimeInstance(DateFormat.SHORT),
447 DateFormat.getTimeInstance(DateFormat.MEDIUM),
448 DateFormat.getTimeInstance(DateFormat.LONG)
449 };
450 for (int i = 0; i < timeFormats.length; i++) {
451 Calendar datetime = new GregorianCalendar();
452 Date date = timeFormats[i].parse(value, position);
453 if (position.getIndex() == value.length()) {
454 datetime.setTime(date);
455 Calendar today = new GregorianCalendar();
456 datetime.set(
457 today.get(Calendar.YEAR),
458 today.get(Calendar.MONTH),
459 today.get(Calendar.DAY_OF_MONTH)
460 );
461 return new Value(datetime.getTime());
462 } else if (position.getIndex() > 0)
463 position.setIndex(0);
464 }
465 throw new ParseException(value, position.getErrorIndex());
466 }
467 }