001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2008, 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-2008, by Object Refinery Limited and Contributors.
031     *
032     * Original Author:  David Gilbert (for Object Refinery Limited);
033     * Contributor(s):   Michael Siemer;
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     * 15-Feb-2008 : Applied patch 1873328 by Michael Siemer, with minor
041     *               modifications (DG);
042     *
043     */
044    
045    package org.jfree.chart.util;
046    
047    import java.text.DateFormat;
048    import java.text.DecimalFormat;
049    import java.text.FieldPosition;
050    import java.text.NumberFormat;
051    import java.text.ParsePosition;
052    import java.util.Calendar;
053    import java.util.Date;
054    import java.util.GregorianCalendar;
055    
056    /**
057     * A formatter that formats dates to show the elapsed time relative to some
058     * base date.
059     *
060     * @since 1.0.3
061     */
062    public class RelativeDateFormat extends DateFormat {
063    
064        /** The base milliseconds for the elapsed time calculation. */
065        private long baseMillis;
066    
067        /**
068         * A flag that controls whether or not a zero day count is displayed.
069         */
070        private boolean showZeroDays;
071    
072        /**
073         * A flag that controls whether or not a zero hour count is displayed.
074         *
075         * @since 1.0.10
076         */
077        private boolean showZeroHours;
078    
079        /**
080         * A formatter for the day count (most likely not critical until the
081         * day count exceeds 999).
082         */
083        private NumberFormat dayFormatter;
084    
085        /**
086         * A prefix prepended to the start of the format if the relative date is
087         * positive.
088         *
089         * @since 1.0.10
090         */
091        private String positivePrefix;
092    
093        /**
094         * A string appended after the day count.
095         */
096        private String daySuffix;
097    
098        /**
099         * A string appended after the hours.
100         */
101        private String hourSuffix;
102    
103        /**
104         * A string appended after the minutes.
105         */
106        private String minuteSuffix;
107    
108        /**
109         * A formatter for the seconds (and milliseconds).
110         */
111        private NumberFormat secondFormatter;
112    
113        /**
114         * A string appended after the seconds.
115         */
116        private String secondSuffix;
117    
118        /**
119         * A constant for the number of milliseconds in one hour.
120         */
121        private static long MILLISECONDS_IN_ONE_HOUR = 60 * 60 * 1000L;
122    
123        /**
124         * A constant for the number of milliseconds in one day.
125         */
126        private static long MILLISECONDS_IN_ONE_DAY = 24 * MILLISECONDS_IN_ONE_HOUR;
127    
128        /**
129         * Creates a new instance.
130         */
131        public RelativeDateFormat() {
132            this(0L);
133        }
134    
135        /**
136         * Creates a new instance.
137         *
138         * @param time  the date/time (<code>null</code> not permitted).
139         */
140        public RelativeDateFormat(Date time) {
141            this(time.getTime());
142        }
143    
144        /**
145         * Creates a new instance.
146         *
147         * @param baseMillis  the time zone (<code>null</code> not permitted).
148         */
149        public RelativeDateFormat(long baseMillis) {
150            super();
151            this.baseMillis = baseMillis;
152            this.showZeroDays = false;
153            this.showZeroHours = true;
154            this.positivePrefix = "";
155            this.dayFormatter = NumberFormat.getInstance();
156            this.daySuffix = "d";
157            this.hourSuffix = "h";
158            this.minuteSuffix = "m";
159            this.secondFormatter = NumberFormat.getNumberInstance();
160            this.secondFormatter.setMaximumFractionDigits(3);
161            this.secondFormatter.setMinimumFractionDigits(3);
162            this.secondSuffix = "s";
163    
164            // we don't use the calendar or numberFormat fields, but equals(Object)
165            // is failing without them being non-null
166            this.calendar = new GregorianCalendar();
167            this.numberFormat = new DecimalFormat("0");
168        }
169    
170        /**
171         * Returns the base date/time used to calculate the elapsed time for
172         * display.
173         *
174         * @return The base date/time in milliseconds since 1-Jan-1970.
175         *
176         * @see #setBaseMillis(long)
177         */
178        public long getBaseMillis() {
179            return this.baseMillis;
180        }
181    
182        /**
183         * Sets the base date/time used to calculate the elapsed time for display.
184         * This should be specified in milliseconds using the same encoding as
185         * <code>java.util.Date</code>.
186         *
187         * @param baseMillis  the base date/time in milliseconds.
188         *
189         * @see #getBaseMillis()
190         */
191        public void setBaseMillis(long baseMillis) {
192            this.baseMillis = baseMillis;
193        }
194    
195        /**
196         * Returns the flag that controls whether or not zero day counts are
197         * shown in the formatted output.
198         *
199         * @return The flag.
200         *
201         * @see #setShowZeroDays(boolean)
202         */
203        public boolean getShowZeroDays() {
204            return this.showZeroDays;
205        }
206    
207        /**
208         * Sets the flag that controls whether or not zero day counts are shown
209         * in the formatted output.
210         *
211         * @param show  the flag.
212         *
213         * @see #getShowZeroDays()
214         */
215        public void setShowZeroDays(boolean show) {
216            this.showZeroDays = show;
217        }
218    
219        /**
220         * Returns the flag that controls whether or not zero hour counts are
221         * shown in the formatted output.
222         *
223         * @return The flag.
224         *
225         * @see #setShowZeroHours(boolean)
226         *
227         * @since 1.0.10
228         */
229        public boolean getShowZeroHours() {
230            return this.showZeroHours;
231        }
232    
233        /**
234         * Sets the flag that controls whether or not zero hour counts are shown
235         * in the formatted output.
236         *
237         * @param show  the flag.
238         *
239         * @see #getShowZeroHours()
240         *
241         * @since 1.0.10
242         */
243        public void setShowZeroHours(boolean show) {
244            this.showZeroHours = show;
245        }
246    
247        /**
248         * Returns the string that is prepended to the format if the relative time
249         * is positive.
250         *
251         * @return The string (never <code>null</code>).
252         *
253         * @see #setPositivePrefix(String)
254         *
255         * @since 1.0.10
256         */
257        public String getPositivePrefix() {
258            return this.positivePrefix;
259        }
260    
261        /**
262         * Sets the string that is prepended to the format if the relative time is
263         * positive.
264         *
265         * @param prefix  the prefix (<code>null</code> not permitted).
266         *
267         * @see #getPositivePrefix()
268         *
269         * @since 1.0.10
270         */
271        public void setPositivePrefix(String prefix) {
272            if (prefix == null) {
273                throw new IllegalArgumentException("Null 'prefix' argument.");
274            }
275            this.positivePrefix = prefix;
276        }
277    
278        /**
279         * Returns the string that is appended to the day count.
280         *
281         * @return The string.
282         *
283         * @see #setDaySuffix(String)
284         */
285        public String getDaySuffix() {
286            return this.daySuffix;
287        }
288    
289        /**
290         * Sets the string that is appended to the day count.
291         *
292         * @param suffix  the suffix (<code>null</code> not permitted).
293         *
294         * @see #getDaySuffix()
295         */
296        public void setDaySuffix(String suffix) {
297            if (suffix == null) {
298                throw new IllegalArgumentException("Null 'suffix' argument.");
299            }
300            this.daySuffix = suffix;
301        }
302    
303        /**
304         * Returns the string that is appended to the hour count.
305         *
306         * @return The string.
307         *
308         * @see #setHourSuffix(String)
309         */
310        public String getHourSuffix() {
311            return this.hourSuffix;
312        }
313    
314        /**
315         * Sets the string that is appended to the hour count.
316         *
317         * @param suffix  the suffix (<code>null</code> not permitted).
318         *
319         * @see #getHourSuffix()
320         */
321        public void setHourSuffix(String suffix) {
322            if (suffix == null) {
323                throw new IllegalArgumentException("Null 'suffix' argument.");
324            }
325            this.hourSuffix = suffix;
326        }
327    
328        /**
329         * Returns the string that is appended to the minute count.
330         *
331         * @return The string.
332         *
333         * @see #setMinuteSuffix(String)
334         */
335        public String getMinuteSuffix() {
336            return this.minuteSuffix;
337        }
338    
339        /**
340         * Sets the string that is appended to the minute count.
341         *
342         * @param suffix  the suffix (<code>null</code> not permitted).
343         *
344         * @see #getMinuteSuffix()
345         */
346        public void setMinuteSuffix(String suffix) {
347            if (suffix == null) {
348                throw new IllegalArgumentException("Null 'suffix' argument.");
349            }
350            this.minuteSuffix = suffix;
351        }
352    
353        /**
354         * Returns the string that is appended to the second count.
355         *
356         * @return The string.
357         *
358         * @see #setSecondSuffix(String)
359         */
360        public String getSecondSuffix() {
361            return this.secondSuffix;
362        }
363    
364        /**
365         * Sets the string that is appended to the second count.
366         *
367         * @param suffix  the suffix (<code>null</code> not permitted).
368         *
369         * @see #getSecondSuffix()
370         */
371        public void setSecondSuffix(String suffix) {
372            if (suffix == null) {
373                throw new IllegalArgumentException("Null 'suffix' argument.");
374            }
375            this.secondSuffix = suffix;
376        }
377    
378        /**
379         * Sets the formatter for the seconds and milliseconds.
380         *
381         * @param formatter  the formatter (<code>null</code> not permitted).
382         */
383        public void setSecondFormatter(NumberFormat formatter) {
384            if (formatter == null) {
385                throw new IllegalArgumentException("Null 'formatter' argument.");
386            }
387            this.secondFormatter = formatter;
388        }
389    
390        /**
391         * Formats the given date as the amount of elapsed time (relative to the
392         * base date specified in the constructor).
393         *
394         * @param date  the date.
395         * @param toAppendTo  the string buffer.
396         * @param fieldPosition  the field position.
397         *
398         * @return The formatted date.
399         */
400        public StringBuffer format(Date date, StringBuffer toAppendTo,
401                                   FieldPosition fieldPosition) {
402            long currentMillis = date.getTime();
403            long elapsed = currentMillis - this.baseMillis;
404            String signPrefix;
405            if (elapsed < 0) {
406                elapsed *= -1L;
407                signPrefix = "-";
408            }
409            else {
410                signPrefix = this.positivePrefix;
411            }
412    
413            long days = elapsed / MILLISECONDS_IN_ONE_DAY;
414            elapsed = elapsed - (days * MILLISECONDS_IN_ONE_DAY);
415            long hours = elapsed / MILLISECONDS_IN_ONE_HOUR;
416            elapsed = elapsed - (hours * MILLISECONDS_IN_ONE_HOUR);
417            long minutes = elapsed / 60000L;
418            elapsed = elapsed - (minutes * 60000L);
419            double seconds = elapsed / 1000.0;
420    
421            toAppendTo.append(signPrefix);
422            if (days != 0 || this.showZeroDays) {
423                toAppendTo.append(this.dayFormatter.format(days) + getDaySuffix());
424            }
425            if (hours != 0 || this.showZeroHours) {
426                toAppendTo.append(String.valueOf(hours) + getHourSuffix());
427            }
428            toAppendTo.append(String.valueOf(minutes) + getMinuteSuffix());
429            toAppendTo.append(this.secondFormatter.format(seconds)
430                    + getSecondSuffix());
431            return toAppendTo;
432        }
433    
434        /**
435         * Parses the given string (not implemented).
436         *
437         * @param source  the date string.
438         * @param pos  the parse position.
439         *
440         * @return <code>null</code>, as this method has not been implemented.
441         */
442        public Date parse(String source, ParsePosition pos) {
443            return null;
444        }
445    
446        /**
447         * Tests this formatter for equality with an arbitrary object.
448         *
449         * @param obj  the object (<code>null</code> permitted).
450         *
451         * @return A boolean.
452         */
453        public boolean equals(Object obj) {
454            if (obj == this) {
455                return true;
456            }
457            if (!(obj instanceof RelativeDateFormat)) {
458                return false;
459            }
460            if (!super.equals(obj)) {
461                return false;
462            }
463            RelativeDateFormat that = (RelativeDateFormat) obj;
464            if (this.baseMillis != that.baseMillis) {
465                return false;
466            }
467            if (this.showZeroDays != that.showZeroDays) {
468                return false;
469            }
470            if (this.showZeroHours != that.showZeroHours) {
471                return false;
472            }
473            if (!this.positivePrefix.equals(that.positivePrefix)) {
474                return false;
475            }
476            if (!this.daySuffix.equals(that.daySuffix)) {
477                return false;
478            }
479            if (!this.hourSuffix.equals(that.hourSuffix)) {
480                return false;
481            }
482            if (!this.minuteSuffix.equals(that.minuteSuffix)) {
483                return false;
484            }
485            if (!this.secondSuffix.equals(that.secondSuffix)) {
486                return false;
487            }
488            if (!this.secondFormatter.equals(that.secondFormatter)) {
489                return false;
490            }
491            return true;
492        }
493    
494        /**
495         * Returns a hash code for this instance.
496         *
497         * @return A hash code.
498         */
499        public int hashCode() {
500            int result = 193;
501            result = 37 * result
502                    + (int) (this.baseMillis ^ (this.baseMillis >>> 32));
503            result = 37 * result + this.positivePrefix.hashCode();
504            result = 37 * result + this.daySuffix.hashCode();
505            result = 37 * result + this.hourSuffix.hashCode();
506            result = 37 * result + this.minuteSuffix.hashCode();
507            result = 37 * result + this.secondSuffix.hashCode();
508            result = 37 * result + this.secondFormatter.hashCode();
509            return result;
510        }
511    
512        /**
513         * Returns a clone of this instance.
514         *
515         * @return A clone.
516         */
517        public Object clone() {
518            RelativeDateFormat clone = (RelativeDateFormat) super.clone();
519            clone.dayFormatter = (NumberFormat) this.dayFormatter.clone();
520            clone.secondFormatter = (NumberFormat) this.secondFormatter.clone();
521            return clone;
522        }
523    
524        /**
525         * Some test code.
526         *
527         * @param args  ignored.
528         */
529        public static void main(String[] args) {
530            GregorianCalendar c0 = new GregorianCalendar(2006, 10, 1, 0, 0, 0);
531            GregorianCalendar c1 = new GregorianCalendar(2006, 10, 1, 11, 37, 43);
532            c1.set(Calendar.MILLISECOND, 123);
533    
534            System.out.println("Default: ");
535            RelativeDateFormat rdf = new RelativeDateFormat(c0.getTime().getTime());
536            System.out.println(rdf.format(c1.getTime()));
537            System.out.println();
538    
539            System.out.println("Hide milliseconds: ");
540            rdf.setSecondFormatter(new DecimalFormat("0"));
541            System.out.println(rdf.format(c1.getTime()));
542            System.out.println();
543    
544            System.out.println("Show zero day output: ");
545            rdf.setShowZeroDays(true);
546            System.out.println(rdf.format(c1.getTime()));
547            System.out.println();
548    
549            System.out.println("Alternative suffixes: ");
550            rdf.setShowZeroDays(false);
551            rdf.setDaySuffix(":");
552            rdf.setHourSuffix(":");
553            rdf.setMinuteSuffix(":");
554            rdf.setSecondSuffix("");
555            System.out.println(rdf.format(c1.getTime()));
556            System.out.println();
557        }
558    }