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    }