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