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
047package org.jfree.chart.util;
048
049import java.text.DateFormat;
050import java.text.DecimalFormat;
051import java.text.FieldPosition;
052import java.text.NumberFormat;
053import java.text.ParsePosition;
054import java.util.Calendar;
055import java.util.Date;
056import 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 */
064public 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}