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