    001/* ===========================================================
    002 * JFreeChart : a free chart library for the Java(tm) platform
    003 * ===========================================================
    004 *
    005 * (C) Copyright 2000-2011, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
    025 * Other names may be trademarks of their respective owners.]
    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}