001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
006 *
007 * Project Info:  http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it 
010 * under the terms of the GNU Lesser General Public License as published by 
011 * the Free Software Foundation; either version 2.1 of the License, or 
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but 
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
022 * USA.  
023 *
024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
025 * in the United States and other countries.]
026 *
027 * -----------------------
028 * RelativeDateFormat.java
029 * -----------------------
030 * (C) Copyright 2006, 2007, by Object Refinery Limited and Contributors.
031 *
032 * Original Author:  David Gilbert (for Object Refinery Limited);
033 * Contributor(s):   -;
034 *
035 * Changes:
036 * --------
037 * 01-Nov-2006 : Version 1 (DG);
038 * 23-Nov-2006 : Added argument checks, updated equals(), added clone() and 
039 *               hashCode() (DG);
040 *
041 */
042package org.jfree.chart.util;
043
044import java.text.DateFormat;
045import java.text.DecimalFormat;
046import java.text.FieldPosition;
047import java.text.NumberFormat;
048import java.text.ParsePosition;
049import java.util.Calendar;
050import java.util.Date;
051import java.util.GregorianCalendar;
052
053/**
054 * A formatter that formats dates to show the elapsed time relative to some
055 * base date.
056 *
057 * @since 1.0.3
058 */
059public class RelativeDateFormat extends DateFormat {
060    
061    /** The base milliseconds for the elapsed time calculation. */
062    private long baseMillis;
063    
064    /**
065     * A flag that controls whether or not a zero day count is displayed.
066     */
067    private boolean showZeroDays;
068    
069    /** 
070     * A formatter for the day count (most likely not critical until the
071     * day count exceeds 999). 
072     */
073    private NumberFormat dayFormatter;
074    
075    /**
076     * A string appended after the day count.
077     */
078    private String daySuffix;
079    
080    /**
081     * A string appended after the hours.
082     */
083    private String hourSuffix;
084    
085    /**
086     * A string appended after the minutes.
087     */
088    private String minuteSuffix;
089    
090    /**
091     * A formatter for the seconds (and milliseconds).
092     */
093    private NumberFormat secondFormatter;
094    
095    /**
096     * A string appended after the seconds.
097     */
098    private String secondSuffix;
099
100    /**
101     * A constant for the number of milliseconds in one hour.
102     */
103    private static long MILLISECONDS_IN_ONE_HOUR = 60 * 60 * 1000L;
104
105    /**
106     * A constant for the number of milliseconds in one day.
107     */
108    private static long MILLISECONDS_IN_ONE_DAY = 24 * MILLISECONDS_IN_ONE_HOUR;
109    
110    /**
111     * Creates a new instance.
112     */
113    public RelativeDateFormat() {
114        this(0L);  
115    }
116    
117    /**
118     * Creates a new instance.
119     * 
120     * @param time  the date/time (<code>null</code> not permitted).
121     */
122    public RelativeDateFormat(Date time) {
123        this(time.getTime());
124    }
125    
126    /**
127     * Creates a new instance.
128     * 
129     * @param baseMillis  the time zone (<code>null</code> not permitted).
130     */
131    public RelativeDateFormat(long baseMillis) {
132        super();        
133        this.baseMillis = baseMillis;
134        this.showZeroDays = false;
135        this.dayFormatter = NumberFormat.getInstance();
136        this.daySuffix = "d";
137        this.hourSuffix = "h";
138        this.minuteSuffix = "m";
139        this.secondFormatter = NumberFormat.getNumberInstance();
140        this.secondFormatter.setMaximumFractionDigits(3);
141        this.secondFormatter.setMinimumFractionDigits(3);
142        this.secondSuffix = "s";
143
144        // we don't use the calendar or numberFormat fields, but equals(Object) 
145        // is failing without them being non-null
146        this.calendar = new GregorianCalendar();
147        this.numberFormat = new DecimalFormat("0");    
148    }
149    
150    /**
151     * Returns the base date/time used to calculate the elapsed time for 
152     * display.
153     * 
154     * @return The base date/time in milliseconds since 1-Jan-1970.
155     * 
156     * @see #setBaseMillis(long)
157     */
158    public long getBaseMillis() {
159        return this.baseMillis;
160    }
161    
162    /**
163     * Sets the base date/time used to calculate the elapsed time for display.  
164     * This should be specified in milliseconds using the same encoding as
165     * <code>java.util.Date</code>.
166     * 
167     * @param baseMillis  the base date/time in milliseconds.
168     * 
169     * @see #getBaseMillis()
170     */
171    public void setBaseMillis(long baseMillis) {
172        this.baseMillis = baseMillis;
173    }
174    
175    /**
176     * Returns the flag that controls whether or not zero day counts are 
177     * shown in the formatted output.
178     * 
179     * @return The flag.
180     * 
181     * @see #setShowZeroDays(boolean)
182     */
183    public boolean getShowZeroDays() {
184        return this.showZeroDays;
185    }
186    
187    /**
188     * Sets the flag that controls whether or not zero day counts are shown
189     * in the formatted output.
190     * 
191     * @param show  the flag.
192     * 
193     * @see #getShowZeroDays()
194     */
195    public void setShowZeroDays(boolean show) {
196        this.showZeroDays = show;
197    }
198    
199    /**
200     * Returns the string that is appended to the day count.
201     * 
202     * @return The string.
203     * 
204     * @see #setDaySuffix(String)
205     */
206    public String getDaySuffix() {
207        return this.daySuffix;
208    }
209    
210    /**
211     * Sets the string that is appended to the day count.
212     * 
213     * @param suffix  the suffix (<code>null</code> not permitted).
214     * 
215     * @see #getDaySuffix()
216     */
217    public void setDaySuffix(String suffix) {
218        if (suffix == null) {
219            throw new IllegalArgumentException("Null 'suffix' argument.");
220        }
221        this.daySuffix = suffix;
222    }
223
224    /**
225     * Returns the string that is appended to the hour count.
226     * 
227     * @return The string.
228     * 
229     * @see #setHourSuffix(String)
230     */
231    public String getHourSuffix() {
232        return this.hourSuffix;
233    }
234    
235    /**
236     * Sets the string that is appended to the hour count.
237     * 
238     * @param suffix  the suffix (<code>null</code> not permitted).
239     * 
240     * @see #getHourSuffix()
241     */
242    public void setHourSuffix(String suffix) {
243        if (suffix == null) {
244            throw new IllegalArgumentException("Null 'suffix' argument.");
245        }
246        this.hourSuffix = suffix;
247    }
248
249    /**
250     * Returns the string that is appended to the minute count.
251     * 
252     * @return The string.
253     * 
254     * @see #setMinuteSuffix(String)
255     */
256    public String getMinuteSuffix() {
257        return this.minuteSuffix;
258    }
259    
260    /**
261     * Sets the string that is appended to the minute count.
262     * 
263     * @param suffix  the suffix (<code>null</code> not permitted).
264     * 
265     * @see #getMinuteSuffix()
266     */
267    public void setMinuteSuffix(String suffix) {
268        if (suffix == null) {
269            throw new IllegalArgumentException("Null 'suffix' argument.");
270        }
271        this.minuteSuffix = suffix;
272    }
273
274    /**
275     * Returns the string that is appended to the second count.
276     * 
277     * @return The string.
278     * 
279     * @see #setSecondSuffix(String)
280     */
281    public String getSecondSuffix() {
282        return this.secondSuffix;
283    }
284    
285    /**
286     * Sets the string that is appended to the second count.
287     * 
288     * @param suffix  the suffix (<code>null</code> not permitted).
289     * 
290     * @see #getSecondSuffix()
291     */
292    public void setSecondSuffix(String suffix) {
293        if (suffix == null) {
294            throw new IllegalArgumentException("Null 'suffix' argument.");
295        }
296        this.secondSuffix = suffix;
297    }
298    
299    /**
300     * Sets the formatter for the seconds and milliseconds.
301     * 
302     * @param formatter  the formatter (<code>null</code> not permitted).
303     */
304    public void setSecondFormatter(NumberFormat formatter) {
305        if (formatter == null) {
306            throw new IllegalArgumentException("Null 'formatter' argument.");
307        }
308        this.secondFormatter = formatter;
309    }
310
311    /**
312     * Formats the given date as the amount of elapsed time (relative to the
313     * base date specified in the constructor).
314     * 
315     * @param date  the date.
316     * @param toAppendTo  the string buffer.
317     * @param fieldPosition  the field position.
318     * 
319     * @return The formatted date.
320     */
321    public StringBuffer format(Date date, StringBuffer toAppendTo,
322                               FieldPosition fieldPosition) {
323        long currentMillis = date.getTime();
324        long elapsed = currentMillis - this.baseMillis;
325        
326        long days = elapsed / MILLISECONDS_IN_ONE_DAY;
327        elapsed = elapsed - (days * MILLISECONDS_IN_ONE_DAY);
328        long hours = elapsed / MILLISECONDS_IN_ONE_HOUR;
329        elapsed = elapsed - (hours * MILLISECONDS_IN_ONE_HOUR);
330        long minutes = elapsed / 60000L;
331        elapsed = elapsed - (minutes * 60000L);
332        double seconds = elapsed / 1000.0;
333        if (days != 0 || this.showZeroDays) {
334            toAppendTo.append(this.dayFormatter.format(days) + getDaySuffix());
335        }
336        toAppendTo.append(String.valueOf(hours) + getHourSuffix());
337        toAppendTo.append(String.valueOf(minutes) + getMinuteSuffix());
338        toAppendTo.append(this.secondFormatter.format(seconds) 
339                + getSecondSuffix());
340        return toAppendTo;   
341    }
342
343    /**
344     * Parses the given string (not implemented).
345     * 
346     * @param source  the date string.
347     * @param pos  the parse position.
348     * 
349     * @return <code>null</code>, as this method has not been implemented.
350     */
351    public Date parse(String source, ParsePosition pos) {
352        return null;   
353    }
354
355    /**
356     * Tests this formatter for equality with an arbitrary object.
357     * 
358     * @param obj  the object (<code>null</code> permitted).
359     * 
360     * @return A boolean.
361     */
362    public boolean equals(Object obj) {
363        if (obj == this) {
364            return true;
365        }
366        if (!(obj instanceof RelativeDateFormat)) {
367            return false;
368        }
369        if (!super.equals(obj)) {
370            return false;
371        }
372        RelativeDateFormat that = (RelativeDateFormat) obj;
373        if (this.baseMillis != that.baseMillis) {
374            return false;
375        }
376        if (this.showZeroDays != that.showZeroDays) {
377            return false;
378        }
379        if (!this.daySuffix.equals(that.daySuffix)) {
380            return false;
381        }
382        if (!this.hourSuffix.equals(that.hourSuffix)) {
383            return false;
384        }
385        if (!this.minuteSuffix.equals(that.minuteSuffix)) {
386            return false;
387        }
388        if (!this.secondSuffix.equals(that.secondSuffix)) {
389            return false;
390        }
391        if (!this.secondFormatter.equals(that.secondFormatter)) {
392            return false;
393        }
394        return true;
395    }
396    
397    /**
398     * Returns a hash code for this instance.
399     * 
400     * @return A hash code.
401     */
402    public int hashCode() {
403        int result = 193;
404        result = 37 * result 
405                + (int) (this.baseMillis ^ (this.baseMillis >>> 32));
406        result = 37 * result + this.daySuffix.hashCode();
407        result = 37 * result + this.hourSuffix.hashCode();
408        result = 37 * result + this.minuteSuffix.hashCode();
409        result = 37 * result + this.secondSuffix.hashCode();
410        result = 37 * result + this.secondFormatter.hashCode();
411        return result;
412    }
413
414    /**
415     * Returns a clone of this instance.
416     * 
417     * @return A clone.
418     */
419    public Object clone() {
420        RelativeDateFormat clone = (RelativeDateFormat) super.clone();
421        clone.dayFormatter = (NumberFormat) this.dayFormatter.clone();
422        clone.secondFormatter = (NumberFormat) this.secondFormatter.clone();
423        return clone;
424    }
425    
426    /**
427     * Some test code.
428     * 
429     * @param args  ignored.
430     */
431    public static void main(String[] args) {
432        GregorianCalendar c0 = new GregorianCalendar(2006, 10, 1, 0, 0, 0);
433        GregorianCalendar c1 = new GregorianCalendar(2006, 10, 1, 11, 37, 43);
434        c1.set(Calendar.MILLISECOND, 123);
435        
436        System.out.println("Default: ");
437        RelativeDateFormat rdf = new RelativeDateFormat(c0.getTimeInMillis());
438        System.out.println(rdf.format(c1.getTime()));
439        System.out.println();
440        
441        System.out.println("Hide milliseconds: ");
442        rdf.setSecondFormatter(new DecimalFormat("0"));
443        System.out.println(rdf.format(c1.getTime()));        
444        System.out.println();
445
446        System.out.println("Show zero day output: ");
447        rdf.setShowZeroDays(true);
448        System.out.println(rdf.format(c1.getTime()));
449        System.out.println();
450        
451        System.out.println("Alternative suffixes: ");
452        rdf.setShowZeroDays(false);
453        rdf.setDaySuffix(":");
454        rdf.setHourSuffix(":");
455        rdf.setMinuteSuffix(":");
456        rdf.setSecondSuffix("");
457        System.out.println(rdf.format(c1.getTime()));
458        System.out.println();
459    }
460}