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 */
021package csheets.core;
022
023import java.io.Serializable;
024import java.text.DateFormat;
025import java.text.Format;
026import java.text.NumberFormat;
027import java.text.ParseException;
028import java.text.ParsePosition;
029import java.util.Arrays;
030import java.util.Calendar;
031import java.util.Date;
032import java.util.GregorianCalendar;
033
034/**
035 * A typed value that a cell can contain.
036 * @author Einar Pehrson
037 */
038public 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}