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 * DateTickUnit.java
029 * -----------------
030 * (C) Copyright 2000-2020, by Object Refinery Limited.
031 *
032 * Original Author:  David Gilbert (for Object Refinery Limited);
033 * Contributor(s):   Chris Boek;
034 *
035 * Changes
036 * -------
037 * 08-Nov-2002 : Moved to new package com.jrefinery.chart.axis (DG);
038 * 27-Nov-2002 : Added IllegalArgumentException to getMillisecondCount()
039 *               method (DG);
040 * 26-Mar-2003 : Implemented Serializable (DG);
041 * 12-Nov-2003 : Added roll fields that can improve the labelling on segmented
042 *               date axes (DG);
043 * 03-Dec-2003 : DateFormat constructor argument is now filled with an default
044 *               if null (TM);
045 * 07-Dec-2003 : Fixed bug (null pointer exception) in constructor (DG);
046 * ------------- JFREECHART 1.0.x ---------------------------------------------
047 * 21-Mar-2007 : Added toString() for debugging (DG);
048 * 04-Apr-2007 : Added new methods addToDate(Date, TimeZone) and rollDate(Date,
049 *               TimeZone) (CB);
050 * 09-Jun-2008 : Deprecated addToDate(Date) (DG);
051 * 09-Jan-2009 : Replaced the unit and rollUnit fields with an enumerated
052 *               type (DG);
053 * 02-Jul-2013 : Use ParamChecks (DG);
054 *
055 */
056
057package org.jfree.chart.axis;
058
059import java.io.Serializable;
060import java.text.DateFormat;
061import java.util.Calendar;
062import java.util.Date;
063import java.util.TimeZone;
064import org.jfree.chart.util.ObjectUtils;
065import org.jfree.chart.util.Args;
066
067/**
068 * A tick unit for use by subclasses of {@link DateAxis}.  Instances of this
069 * class are immutable.
070 */
071public class DateTickUnit extends TickUnit implements Serializable {
072
073    /** For serialization. */
074    private static final long serialVersionUID = -7289292157229621901L;
075
076    /**
077     * The units.
078     *
079     * @since 1.0.13
080     */
081    private DateTickUnitType unitType;
082
083    /** The unit count. */
084    private int count;
085
086    /**
087     * The roll unit type.
088     *
089     * @since 1.0.13
090     */
091    private DateTickUnitType rollUnitType;
092
093    /** The roll count. */
094    private int rollCount;
095
096    /** The date formatter. */
097    private DateFormat formatter;
098
099    /**
100     * Creates a new date tick unit.
101     *
102     * @param unitType  the unit type ({@code null} not permitted).
103     * @param multiple  the multiple (of the unit type, must be > 0).
104     *
105     * @since 1.0.13
106     */
107    public DateTickUnit(DateTickUnitType unitType, int multiple) {
108        this(unitType, multiple, DateFormat.getDateInstance(DateFormat.SHORT));
109    }
110
111    /**
112     * Creates a new date tick unit.
113     *
114     * @param unitType  the unit type ({@code null} not permitted).
115     * @param multiple  the multiple (of the unit type, must be > 0).
116     * @param formatter  the date formatter ({@code null} not permitted).
117     *
118     * @since 1.0.13
119     */
120    public DateTickUnit(DateTickUnitType unitType, int multiple,
121            DateFormat formatter) {
122        this(unitType, multiple, unitType, multiple, formatter);
123    }
124
125    /**
126     * Creates a new unit.
127     *
128     * @param unitType  the unit.
129     * @param multiple  the multiple.
130     * @param rollUnitType  the roll unit.
131     * @param rollMultiple  the roll multiple.
132     * @param formatter  the date formatter ({@code null} not permitted).
133     *
134     * @since 1.0.13
135     */
136    public DateTickUnit(DateTickUnitType unitType, int multiple,
137            DateTickUnitType rollUnitType, int rollMultiple,
138            DateFormat formatter) {
139        super(DateTickUnit.getMillisecondCount(unitType, multiple));
140        Args.nullNotPermitted(formatter, "formatter");
141        if (multiple <= 0) {
142            throw new IllegalArgumentException("Requires 'multiple' > 0.");
143        }
144        if (rollMultiple <= 0) {
145            throw new IllegalArgumentException("Requires 'rollMultiple' > 0.");
146        }
147        this.unitType = unitType;
148        this.count = multiple;
149        this.rollUnitType = rollUnitType;
150        this.rollCount = rollMultiple;
151        this.formatter = formatter;
152    }
153
154    /**
155     * Returns the unit type.
156     *
157     * @return The unit type (never {@code null}).
158     *
159     * @since 1.0.13
160     */
161    public DateTickUnitType getUnitType() {
162        return this.unitType;
163    }
164
165    /**
166     * Returns the unit multiple.
167     *
168     * @return The unit multiple (always &gt; 0).
169     */
170    public int getMultiple() {
171        return this.count;
172    }
173
174    /**
175     * Returns the roll unit type.
176     *
177     * @return The roll unit type (never {@code null}).
178     *
179     * @since 1.0.13
180     */
181    public DateTickUnitType getRollUnitType() {
182        return this.rollUnitType;
183    }
184
185    /**
186     * Returns the roll unit multiple.
187     *
188     * @return The roll unit multiple.
189     *
190     * @since 1.0.13
191     */
192    public int getRollMultiple() {
193        return this.rollCount;
194    }
195
196    /**
197     * Formats a value.
198     *
199     * @param milliseconds  date in milliseconds since 01-01-1970.
200     *
201     * @return The formatted date.
202     */
203    @Override
204    public String valueToString(double milliseconds) {
205        return this.formatter.format(new Date((long) milliseconds));
206    }
207
208    /**
209     * Formats a date using the tick unit's formatter.
210     *
211     * @param date  the date.
212     *
213     * @return The formatted date.
214     */
215    public String dateToString(Date date) {
216        return this.formatter.format(date);
217    }
218
219    /**
220     * Calculates a new date by adding this unit to the base date.
221     *
222     * @param base  the base date.
223     * @param zone  the time zone for the date calculation.
224     *
225     * @return A new date one unit after the base date.
226     *
227     * @since 1.0.6
228     */
229    public Date addToDate(Date base, TimeZone zone) {
230        // as far as I know, the Locale for the calendar only affects week
231        // number calculations, and since DateTickUnit doesn't do week
232        // arithmetic, the default locale (whatever it is) should be fine
233        // here...
234        Calendar calendar = Calendar.getInstance(zone);
235        calendar.setTime(base);
236        calendar.add(this.unitType.getCalendarField(), this.count);
237        return calendar.getTime();
238    }
239
240    /**
241     * Rolls the date forward by the amount specified by the roll unit and
242     * count.
243     *
244     * @param base  the base date.
245
246     * @return The rolled date.
247     *
248     * @see #rollDate(Date, TimeZone)
249     */
250    public Date rollDate(Date base) {
251        return rollDate(base, TimeZone.getDefault());
252    }
253
254    /**
255     * Rolls the date forward by the amount specified by the roll unit and
256     * count.
257     *
258     * @param base  the base date.
259     * @param zone  the time zone.
260     *
261     * @return The rolled date.
262     *
263     * @since 1.0.6
264     */
265    public Date rollDate(Date base, TimeZone zone) {
266        // as far as I know, the Locale for the calendar only affects week
267        // number calculations, and since DateTickUnit doesn't do week
268        // arithmetic, the default locale (whatever it is) should be fine
269        // here...
270        Calendar calendar = Calendar.getInstance(zone);
271        calendar.setTime(base);
272        calendar.add(this.rollUnitType.getCalendarField(), this.rollCount);
273        return calendar.getTime();
274    }
275
276    /**
277     * Returns a field code that can be used with the {@code Calendar}
278     * class.
279     *
280     * @return The field code.
281     */
282    public int getCalendarField() {
283        return this.unitType.getCalendarField();
284    }
285
286    /**
287     * Returns the (approximate) number of milliseconds for the given unit and
288     * unit count.
289     * <P>
290     * This value is an approximation some of the time (e.g. months are
291     * assumed to have 31 days) but this shouldn't matter.
292     *
293     * @param unit  the unit.
294     * @param count  the unit count.
295     *
296     * @return The number of milliseconds.
297     *
298     * @since 1.0.13
299     */
300    private static long getMillisecondCount(DateTickUnitType unit, int count) {
301
302        if (unit.equals(DateTickUnitType.YEAR)) {
303            return (365L * 24L * 60L * 60L * 1000L) * count;
304        }
305        else if (unit.equals(DateTickUnitType.MONTH)) {
306            return (31L * 24L * 60L * 60L * 1000L) * count;
307        }
308        else if (unit.equals(DateTickUnitType.DAY)) {
309            return (24L * 60L * 60L * 1000L) * count;
310        }
311        else if (unit.equals(DateTickUnitType.HOUR)) {
312            return (60L * 60L * 1000L) * count;
313        }
314        else if (unit.equals(DateTickUnitType.MINUTE)) {
315            return (60L * 1000L) * count;
316        }
317        else if (unit.equals(DateTickUnitType.SECOND)) {
318            return 1000L * count;
319        }
320        else if (unit.equals(DateTickUnitType.MILLISECOND)) {
321            return count;
322        }
323        else {
324            throw new IllegalArgumentException("The 'unit' argument has a " 
325                    + "value that is not recognised.");
326        }
327
328    }
329
330    /**
331     * Tests this unit for equality with another object.
332     *
333     * @param obj  the object ({@code null} permitted).
334     *
335     * @return {@code true} or {@code false}.
336     */
337    @Override
338    public boolean equals(Object obj) {
339        if (obj == this) {
340            return true;
341        }
342        if (!(obj instanceof DateTickUnit)) {
343            return false;
344        }
345        if (!super.equals(obj)) {
346            return false;
347        }
348        DateTickUnit that = (DateTickUnit) obj;
349        if (!(this.unitType.equals(that.unitType))) {
350            return false;
351        }
352        if (this.count != that.count) {
353            return false;
354        }
355        if (!ObjectUtils.equal(this.formatter, that.formatter)) {
356            return false;
357        }
358        return true;
359    }
360
361    /**
362     * Returns a hash code for this object.
363     *
364     * @return A hash code.
365     */
366    @Override
367    public int hashCode() {
368        int result = 19;
369        result = 37 * result + this.unitType.hashCode();
370        result = 37 * result + this.count;
371        result = 37 * result + this.formatter.hashCode();
372        return result;
373    }
374
375    /**
376     * Returns a string representation of this instance, primarily used for
377     * debugging purposes.
378     *
379     * @return A string representation of this instance.
380     */
381    @Override
382    public String toString() {
383        return "DateTickUnit[" + this.unitType.toString() + ", "
384                + this.count + "]";
385    }
386
387}