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     */
042    package org.jfree.chart.util;
043    
044    import java.text.DateFormat;
045    import java.text.DecimalFormat;
046    import java.text.FieldPosition;
047    import java.text.NumberFormat;
048    import java.text.ParsePosition;
049    import java.util.Calendar;
050    import java.util.Date;
051    import 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     */
059    public 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    }