    001/* ===========================================================
    002 * JFreeChart : a free chart library for the Java(tm) platform
    003 * ===========================================================
    004 *
    005 * (C) Copyright 2000-2011, 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 * 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
    066package org.jfree.data.time;
    067
    068import java.io.Serializable;
    069import java.util.Calendar;
    070import java.util.Date;
    071import java.util.Locale;
    072import java.util.TimeZone;
    073
    074import org.jfree.date.MonthConstants;
    075import 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 */
    081public 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}