    001    /* ===========================================================
    002     * JFreeChart : a free chart library for the Java(tm) platform
    003     * ===========================================================
    004     *
    005     * (C) Copyright 2000-2009, 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     * Month.java
    029     * ----------
    030     * (C) Copyright 2001-2009, by Object Refinery Limited and Contributors.
    031     *
    032     * Original Author:  David Gilbert (for Object Refinery Limited);
    033     * Contributor(s):   Chris Boek;
    034     *
    035     * Changes
    036     * -------
    037     * 11-Oct-2001 : Version 1 (DG);
    038     * 14-Nov-2001 : Added method to get year as primitive (DG);
    039     *               Override for toString() method (DG);
    040     * 18-Dec-2001 : Changed order of parameters in constructor (DG);
    041     * 19-Dec-2001 : Added a new constructor as suggested by Paul English (DG);
    042     * 29-Jan-2002 : Worked on the parseMonth() method (DG);
    043     * 14-Feb-2002 : Fixed bugs in the Month constructors (DG);
    044     * 26-Feb-2002 : Changed getStart(), getMiddle() and getEnd() methods to
    045     *               evaluate with reference to a particular time zone (DG);
    046     * 19-Mar-2002 : Changed API for TimePeriod classes (DG);
    047     * 10-Sep-2002 : Added getSerialIndex() method (DG);
    048     * 04-Oct-2002 : Fixed errors reported by Checkstyle (DG);
    049     * 10-Jan-2003 : Changed base class and method names (DG);
    050     * 13-Mar-2003 : Moved to com.jrefinery.data.time package, and implemented
    051     *               Serializable (DG);
    052     * 21-Oct-2003 : Added hashCode() method (DG);
    053     * 01-Nov-2005 : Fixed bug 1345383 (argument check in constructor) (DG);
    054     * ------------- JFREECHART 1.0.x ---------------------------------------------
    055     * 05-Oct-2006 : Updated API docs (DG);
    056     * 06-Oct-2006 : Refactored to cache first and last millisecond values (DG);
    057     * 04-Apr-2007 : Fixed bug in Month(Date, TimeZone) constructor (CB);
    058     * 01-Sep-2008 : Added clarification for previous() and next() methods (DG);
    059     * 16-Sep-2008 : Deprecated DEFAULT_TIME_ZONE, and updated parsing to handle
    060     *               extended range in Year (DG);
    061     * 25-Nov-2008 : Added new constructor with Locale (DG);
    062     * 04-Feb-2009 : Fix for new constructor with Locale - bug 2564636 (DG);
    063     *
    064     */
    065
    066    package org.jfree.data.time;
    067
    068    import java.io.Serializable;
    069    import java.util.Calendar;
    070    import java.util.Date;
    071    import java.util.Locale;
    072    import java.util.TimeZone;
    073
    074    import org.jfree.date.MonthConstants;
    075    import org.jfree.date.SerialDate;
    076
    077    /**
    078     * Represents a single month.  This class is immutable, which is a requirement
    079     * for all {@link RegularTimePeriod} subclasses.
    080     */
    081    public class Month extends RegularTimePeriod implements Serializable {
    082
    083        /** For serialization. */
    084        private static final long serialVersionUID = -5090216912548722570L;
    085
    086        /** The month (1-12). */
    087        private int month;
    088
    089        /** The year in which the month falls. */
    090        private int year;
    091
    092        /** The first millisecond. */
    093        private long firstMillisecond;
    094
    095        /** The last millisecond. */
    096        private long lastMillisecond;
    097
    098        /**
    099         * Constructs a new Month, based on the current system time.
    100         */
    101        public Month() {
    102            this(new Date());
    103        }
    104
    105        /**
    106         * Constructs a new month instance.
    107         *
    108         * @param month  the month (in the range 1 to 12).
    109         * @param year  the year.
    110         */
    111        public Month(int month, int year) {
    112            if ((month < 1) || (month > 12)) {
    113                throw new IllegalArgumentException("Month outside valid range.");
    114            }
    115            this.month = month;
    116            this.year = year;
    117            peg(Calendar.getInstance());
    118        }
    119
    120        /**
    121         * Constructs a new month instance.
    122         *
    123         * @param month  the month (in the range 1 to 12).
    124         * @param year  the year.
    125         */
    126        public Month(int month, Year year) {
    127            if ((month < 1) || (month > 12)) {
    128                throw new IllegalArgumentException("Month outside valid range.");
    129            }
    130            this.month = month;
    131            this.year = year.getYear();
    132            peg(Calendar.getInstance());
    133        }
    134
    135        /**
    136         * Constructs a new <code>Month</code> instance, based on a date/time and
    137         * the default time zone.
    138         *
    139         * @param time  the date/time (<code>null</code> not permitted).
    140         *
    141         * @see #Month(Date, TimeZone)
    142         */
    143        public Month(Date time) {
    144            this(time, TimeZone.getDefault());
    145        }
    146
    147        /**
    148         * Constructs a new <code>Month</code> instance, based on a date/time and
    149         * a time zone.  The first and last millisecond values are initially
    150         * pegged to the given time zone also.
    151         *
    152         * @param time  the date/time.
    153         * @param zone  the time zone (<code>null</code> not permitted).
    154         *
    155         * @deprecated Since 1.0.12, use {@link #Month(Date, TimeZone, Locale)}
    156         *     instead.
    157         */
    158        public Month(Date time, TimeZone zone) {
    159            this(time, zone, Locale.getDefault());
    160        }
    161
    162        /**
    163         * Creates a new <code>Month</code> instance, based on the specified time,
    164         * zone and locale.
    165         *
    166         * @param time  the current time.
    167         * @param zone  the time zone.
    168         * @param locale  the locale.
    169         *
    170         * @since 1.0.12
    171         */
    172        public Month(Date time, TimeZone zone, Locale locale) {
    173            Calendar calendar = Calendar.getInstance(zone, locale);
    174            calendar.setTime(time);
    175            this.month = calendar.get(Calendar.MONTH) + 1;
    176            this.year = calendar.get(Calendar.YEAR);
    177            peg(calendar);
    178        }
    179
    180        /**
    181         * Returns the year in which the month falls.
    182         *
    183         * @return The year in which the month falls (as a Year object).
    184         */
    185        public Year getYear() {
    186            return new Year(this.year);
    187        }
    188
    189        /**
    190         * Returns the year in which the month falls.
    191         *
    192         * @return The year in which the month falls (as an int).
    193         */
    194        public int getYearValue() {
    195            return this.year;
    196        }
    197
    198        /**
    199         * Returns the month.  Note that 1=JAN, 2=FEB, ...
    200         *
    201         * @return The month.
    202         */
    203        public int getMonth() {
    204            return this.month;
    205        }
    206
    207        /**
    208         * Returns the first millisecond of the month.  This will be determined
    209         * relative to the time zone specified in the constructor, or in the
    210         * calendar instance passed in the most recent call to the
    211         * {@link #peg(Calendar)} method.
    212         *
    213         * @return The first millisecond of the month.
    214         *
    215         * @see #getLastMillisecond()
    216         */
    217        public long getFirstMillisecond() {
    218            return this.firstMillisecond;
    219        }
    220
    221        /**
    222         * Returns the last millisecond of the month.  This will be
    223         * determined relative to the time zone specified in the constructor, or
    224         * in the calendar instance passed in the most recent call to the
    225         * {@link #peg(Calendar)} method.
    226         *
    227         * @return The last millisecond of the month.
    228         *
    229         * @see #getFirstMillisecond()
    230         */
    231        public long getLastMillisecond() {
    232            return this.lastMillisecond;
    233        }
    234
    235        /**
    236         * Recalculates the start date/time and end date/time for this time period
    237         * relative to the supplied calendar (which incorporates a time zone).
    238         *
    239         * @param calendar  the calendar (<code>null</code> not permitted).
    240         *
    241         * @since 1.0.3
    242         */
    243        public void peg(Calendar calendar) {
    244            this.firstMillisecond = getFirstMillisecond(calendar);
    245            this.lastMillisecond = getLastMillisecond(calendar);
    246        }
    247
    248        /**
    249         * Returns the month preceding this one.  Note that the returned
    250         * {@link Month} is "pegged" using the default time-zone, irrespective of
    251         * the time-zone used to peg of the current month (which is not recorded
    252         * anywhere).  See the {@link #peg(Calendar)} method.
    253         *
    254         * @return The month preceding this one.
    255         */
    256        public RegularTimePeriod previous() {
    257            Month result;
    258            if (this.month != MonthConstants.JANUARY) {
    259                result = new Month(this.month - 1, this.year);
    260            }
    261            else {
    262                if (this.year > 1900) {
    263                    result = new Month(MonthConstants.DECEMBER, this.year - 1);
    264                }
    265                else {
    266                    result = null;
    267                }
    268            }
    269            return result;
    270        }
    271
    272        /**
    273         * Returns the month following this one.  Note that the returned
    274         * {@link Month} is "pegged" using the default time-zone, irrespective of
    275         * the time-zone used to peg of the current month (which is not recorded
    276         * anywhere).  See the {@link #peg(Calendar)} method.
    277         *
    278         * @return The month following this one.
    279         */
    280        public RegularTimePeriod next() {
    281            Month result;
    282            if (this.month != MonthConstants.DECEMBER) {
    283                result = new Month(this.month + 1, this.year);
    284            }
    285            else {
    286                if (this.year < 9999) {
    287                    result = new Month(MonthConstants.JANUARY, this.year + 1);
    288                }
    289                else {
    290                    result = null;
    291                }
    292            }
    293            return result;
    294        }
    295
    296        /**
    297         * Returns a serial index number for the month.
    298         *
    299         * @return The serial index number.
    300         */
    301        public long getSerialIndex() {
    302            return this.year * 12L + this.month;
    303        }
    304
    305        /**
    306         * Returns a string representing the month (e.g. "January 2002").
    307         * <P>
    308         * To do: look at internationalisation.
    309         *
    310         * @return A string representing the month.
    311         */
    312        public String toString() {
    313            return SerialDate.monthCodeToString(this.month) + " " + this.year;
    314        }
    315
    316        /**
    317         * Tests the equality of this Month object to an arbitrary object.
    318         * Returns true if the target is a Month instance representing the same
    319         * month as this object.  In all other cases, returns false.
    320         *
    321         * @param obj  the object (<code>null</code> permitted).
    322         *
    323         * @return <code>true</code> if month and year of this and object are the
    324         *         same.
    325         */
    326        public boolean equals(Object obj) {
    327            if (obj == this) {
    328                return true;
    329            }
    330            if (!(obj instanceof Month)) {
    331                return false;
    332            }
    333            Month that = (Month) obj;
    334            if (this.month != that.month) {
    335                return false;
    336            }
    337            if (this.year != that.year) {
    338                return false;
    339            }
    340            return true;
    341        }
    342
    343        /**
    344         * Returns a hash code for this object instance.  The approach described by
    345         * Joshua Bloch in "Effective Java" has been used here:
    346         * <p>
    347         * <code>http://developer.java.sun.com/developer/Books/effectivejava
    348         * /Chapter3.pdf</code>
    349         *
    350         * @return A hash code.
    351         */
    352        public int hashCode() {
    353            int result = 17;
    354            result = 37 * result + this.month;
    355            result = 37 * result + this.year;
    356            return result;
    357        }
    358
    359        /**
    360         * Returns an integer indicating the order of this Month object relative to
    361         * the specified
    362         * object: negative == before, zero == same, positive == after.
    363         *
    364         * @param o1  the object to compare.
    365         *
    366         * @return negative == before, zero == same, positive == after.
    367         */
    368        public int compareTo(Object o1) {
    369
    370            int result;
    371
    372            // CASE 1 : Comparing to another Month object
    373            // --------------------------------------------
    374            if (o1 instanceof Month) {
    375                Month m = (Month) o1;
    376                result = this.year - m.getYearValue();
    377                if (result == 0) {
    378                    result = this.month - m.getMonth();
    379                }
    380            }
    381
    382            // CASE 2 : Comparing to another TimePeriod object
    383            // -----------------------------------------------
    384            else if (o1 instanceof RegularTimePeriod) {
    385                // more difficult case - evaluate later...
    386                result = 0;
    387            }
    388
    389            // CASE 3 : Comparing to a non-TimePeriod object
    390            // ---------------------------------------------
    391            else {
    392                // consider time periods to be ordered after general objects
    393                result = 1;
    394            }
    395
    396            return result;
    397
    398        }
    399
    400        /**
    401         * Returns the first millisecond of the month, evaluated using the supplied
    402         * calendar (which determines the time zone).
    403         *
    404         * @param calendar  the calendar (<code>null</code> not permitted).
    405         *
    406         * @return The first millisecond of the month.
    407         *
    408         * @throws NullPointerException if <code>calendar</code> is
    409         *     <code>null</code>.
    410         */
    411        public long getFirstMillisecond(Calendar calendar) {
    412            calendar.set(this.year, this.month - 1, 1, 0, 0, 0);
    413            calendar.set(Calendar.MILLISECOND, 0);
    414            // in the following line, we'd rather call calendar.getTimeInMillis()
    415            // to avoid object creation, but that isn't supported in Java 1.3.1
    416            return calendar.getTime().getTime();
    417        }
    418
    419        /**
    420         * Returns the last millisecond of the month, evaluated using the supplied
    421         * calendar (which determines the time zone).
    422         *
    423         * @param calendar  the calendar (<code>null</code> not permitted).
    424         *
    425         * @return The last millisecond of the month.
    426         *
    427         * @throws NullPointerException if <code>calendar</code> is
    428         *     <code>null</code>.
    429         */
    430        public long getLastMillisecond(Calendar calendar) {
    431            int eom = SerialDate.lastDayOfMonth(this.month, this.year);
    432            calendar.set(this.year, this.month - 1, eom, 23, 59, 59);
    433            calendar.set(Calendar.MILLISECOND, 999);
    434            // in the following line, we'd rather call calendar.getTimeInMillis()
    435            // to avoid object creation, but that isn't supported in Java 1.3.1
    436            return calendar.getTime().getTime();
    437        }
    438
    439        /**
    440         * Parses the string argument as a month.  This method is required to
    441         * accept the format "YYYY-MM".  It will also accept "MM-YYYY". Anything
    442         * else, at the moment, is a bonus.
    443         *
    444         * @param s  the string to parse (<code>null</code> permitted).
    445         *
    446         * @return <code>null</code> if the string is not parseable, the month
    447         *         otherwise.
    448         */
    449        public static Month parseMonth(String s) {
    450            Month result = null;
    451            if (s == null) {
    452                return result;
    453            }
    454            // trim whitespace from either end of the string
    455            s = s.trim();
    456            int i = Month.findSeparator(s);
    457            String s1, s2;
    458            boolean yearIsFirst;
    459            // if there is no separator, we assume the first four characters
    460            // are YYYY
    461            if (i == -1) {
    462                yearIsFirst = true;
    463                s1 = s.substring(0, 5);
    464                s2 = s.substring(5);
    465            }
    466            else {
    467                s1 = s.substring(0, i).trim();
    468                s2 = s.substring(i + 1, s.length()).trim();
    469                // now it is trickier to determine if the month or year is first
    470                Year y1 = Month.evaluateAsYear(s1);
    471                if (y1 == null) {
    472                    yearIsFirst = false;
    473                }
    474                else {
    475                    Year y2 = Month.evaluateAsYear(s2);
    476                    if (y2 == null) {
    477                        yearIsFirst = true;
    478                    }
    479                    else {
    480                        yearIsFirst = (s1.length() > s2.length());
    481                    }
    482                }
    483            }
    484            Year year;
    485            int month;
    486            if (yearIsFirst) {
    487                year = Month.evaluateAsYear(s1);
    488                month = SerialDate.stringToMonthCode(s2);
    489            }
    490            else {
    491                year = Month.evaluateAsYear(s2);
    492                month = SerialDate.stringToMonthCode(s1);
    493            }
    494            if (month == -1) {
    495                throw new TimePeriodFormatException("Can't evaluate the month.");
    496            }
    497            if (year == null) {
    498                throw new TimePeriodFormatException("Can't evaluate the year.");
    499            }
    500            result = new Month(month, year);
    501            return result;
    502        }
    503
    504        /**
    505         * Finds the first occurrence of '-', or if that character is not found,
    506         * the first occurrence of ',', or the first occurrence of ' ' or '.'
    507         *
    508         * @param s  the string to parse.
    509         *
    510         * @return The position of the separator character, or <code>-1</code> if
    511         *     none of the characters were found.
    512         */
    513        private static int findSeparator(String s) {
    514            int result = s.indexOf('-');
    515            if (result == -1) {
    516                result = s.indexOf(',');
    517            }
    518            if (result == -1) {
    519                result = s.indexOf(' ');
    520            }
    521            if (result == -1) {
    522                result = s.indexOf('.');
    523            }
    524            return result;
    525        }
    526
    527        /**
    528         * Creates a year from a string, or returns <code>null</code> (format
    529         * exceptions suppressed).
    530         *
    531         * @param s  the string to parse.
    532         *
    533         * @return <code>null</code> if the string is not parseable, the year
    534         *         otherwise.
    535         */
    536        private static Year evaluateAsYear(String s) {
    537            Year result = null;
    538            try {
    539                result = Year.parseYear(s);
    540            }
    541            catch (TimePeriodFormatException e) {
    542                // suppress
    543            }
    544            return result;
    545        }
    546
    547    }