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