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