    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    }