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