    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     * Week.java
    029     * ---------
    030     * (C) Copyright 2001-2007, by Object Refinery Limited and Contributors.
    031     *
    032     * Original Author:  David Gilbert (for Object Refinery Limited);
    033     * Contributor(s):   Aimin Han;
    034     *
    035     * $Id: Week.java,v 1.7.2.5 2007/01/10 11:43:46 mungady Exp $
    036     *
    037     * Changes
    038     * -------
    039     * 11-Oct-2001 : Version 1 (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 parseWeek() method (DG);
    043     * 13-Feb-2002 : Fixed bug in Week(Date) constructor (DG);
    044     * 26-Feb-2002 : Changed getStart(), getMiddle() and getEnd() methods to
    045     *               evaluate with reference to a particular time zone (DG);
    046     * 05-Apr-2002 : Reinstated this class to the JCommon library (DG);
    047     * 24-Jun-2002 : Removed unnecessary main method (DG);
    048     * 10-Sep-2002 : Added getSerialIndex() method (DG);
    049     * 06-Oct-2002 : Fixed errors reported by Checkstyle (DG);
    050     * 18-Oct-2002 : Changed to observe 52 or 53 weeks per year, consistent with
    051     *               GregorianCalendar. Thanks to Aimin Han for the code (DG);
    052     * 02-Jan-2003 : Removed debug code (DG);
    053     * 13-Mar-2003 : Moved to com.jrefinery.data.time package, and implemented
    054     *               Serializable (DG);
    055     * 21-Oct-2003 : Added hashCode() method (DG);
    056     * 24-May-2004 : Modified getFirstMillisecond() and getLastMillisecond() to
    057     *               take account of firstDayOfWeek setting in Java's Calendar
    058     *               class (DG);
    059     * 30-Sep-2004 : Replaced getTime().getTime() with getTimeInMillis() (DG);
    060     * 04-Nov-2004 : Reverted change of 30-Sep-2004, because it won't work for
    061     *               JDK 1.3 (DG);
    062     * ------------- JFREECHART 1.0.x ---------------------------------------------
    063     * 06-Mar-2006 : Fix for bug 1448828, incorrect calculation of week and year
    064     *               for the first few days of some years (DG);
    065     * 05-Oct-2006 : Updated API docs (DG);
    066     * 06-Oct-2006 : Refactored to cache first and last millisecond values (DG);
    067     * 09-Jan-2007 : Fixed bug in next() (DG);
    068     *
    069     */
    070
    071    package org.jfree.data.time;
    072
    073    import java.io.Serializable;
    074    import java.util.Calendar;
    075    import java.util.Date;
    076    import java.util.TimeZone;
    077
    078    /**
    079     * A calendar week.  All years are considered to have 53 weeks, numbered from 1
    080     * to 53, although in many cases the 53rd week is empty.  Most of the time, the
    081     * 1st week of the year *begins* in the previous calendar year, but it always
    082     * finishes in the current year (this behaviour matches the workings of the
    083     * <code>GregorianCalendar</code> class).
    084     * <P>
    085     * This class is immutable, which is a requirement for all
    086     * {@link RegularTimePeriod} subclasses.
    087     */
    088    public class Week extends RegularTimePeriod implements Serializable {
    089
    090        /** For serialization. */
    091        private static final long serialVersionUID = 1856387786939865061L;
    092
    093        /** Constant for the first week in the year. */
    094        public static final int FIRST_WEEK_IN_YEAR = 1;
    095
    096        /** Constant for the last week in the year. */
    097        public static final int LAST_WEEK_IN_YEAR = 53;
    098
    099        /** The year in which the week falls. */
    100        private short year;
    101
    102        /** The week (1-53). */
    103        private byte week;
    104
    105        /** The first millisecond. */
    106        private long firstMillisecond;
    107
    108        /** The last millisecond. */
    109        private long lastMillisecond;
    110
    111        /**
    112         * Creates a new time period for the week in which the current system
    113         * date/time falls.
    114         */
    115        public Week() {
    116            this(new Date());
    117        }
    118
    119        /**
    120         * Creates a time period representing the week in the specified year.
    121         *
    122         * @param week  the week (1 to 53).
    123         * @param year  the year (1900 to 9999).
    124         */
    125        public Week(int week, int year) {
    126            if ((week < FIRST_WEEK_IN_YEAR) && (week > LAST_WEEK_IN_YEAR)) {
    127                throw new IllegalArgumentException(
    128                        "The 'week' argument must be in the range 1 - 53.");
    129            }
    130            this.week = (byte) week;
    131            this.year = (short) year;
    132            peg(Calendar.getInstance());
    133        }
    134
    135        /**
    136         * Creates a time period representing the week in the specified year.
    137         *
    138         * @param week  the week (1 to 53).
    139         * @param year  the year (1900 to 9999).
    140         */
    141        public Week(int week, Year year) {
    142            if ((week < FIRST_WEEK_IN_YEAR) && (week > LAST_WEEK_IN_YEAR)) {
    143                throw new IllegalArgumentException(
    144                        "The 'week' argument must be in the range 1 - 53.");
    145            }
    146            this.week = (byte) week;
    147            this.year = (short) year.getYear();
    148            peg(Calendar.getInstance());
    149       }
    150
    151        /**
    152         * Creates a time period for the week in which the specified date/time
    153         * falls.
    154         *
    155         * @param time  the time (<code>null</code> not permitted).
    156         */
    157        public Week(Date time) {
    158            // defer argument checking...
    159            this(time, RegularTimePeriod.DEFAULT_TIME_ZONE);
    160        }
    161
    162        /**
    163         * Creates a time period for the week in which the specified date/time
    164         * falls, calculated relative to the specified time zone.
    165         *
    166         * @param time  the date/time (<code>null</code> not permitted).
    167         * @param zone  the time zone (<code>null</code> not permitted).
    168         */
    169        public Week(Date time, TimeZone zone) {
    170            if (time == null) {
    171                throw new IllegalArgumentException("Null 'time' argument.");
    172            }
    173            if (zone == null) {
    174                throw new IllegalArgumentException("Null 'zone' argument.");
    175            }
    176            Calendar calendar = Calendar.getInstance(zone);
    177            calendar.setTime(time);
    178
    179            // sometimes the last few days of the year are considered to fall in
    180            // the *first* week of the following year.  Refer to the Javadocs for
    181            // GregorianCalendar.
    182            int tempWeek = calendar.get(Calendar.WEEK_OF_YEAR);
    183            if (tempWeek == 1
    184                    && calendar.get(Calendar.MONTH) == Calendar.DECEMBER) {
    185                this.week = 1;
    186                this.year = (short) (calendar.get(Calendar.YEAR) + 1);
    187            }
    188            else {
    189                this.week = (byte) Math.min(tempWeek, LAST_WEEK_IN_YEAR);
    190                int yyyy = calendar.get(Calendar.YEAR);
    191                // alternatively, sometimes the first few days of the year are
    192                // considered to fall in the *last* week of the previous year...
    193                if (calendar.get(Calendar.MONTH) == Calendar.JANUARY
    194                        && this.week >= 52) {
    195                    yyyy--;
    196                }
    197                this.year = (short) yyyy;
    198            }
    199            peg(calendar);
    200
    201        }
    202
    203        /**
    204         * Returns the year in which the week falls.
    205         *
    206         * @return The year (never <code>null</code>).
    207         */
    208        public Year getYear() {
    209            return new Year(this.year);
    210        }
    211
    212        /**
    213         * Returns the year in which the week falls, as an integer value.
    214         *
    215         * @return The year.
    216         */
    217        public int getYearValue() {
    218            return this.year;
    219        }
    220
    221        /**
    222         * Returns the week.
    223         *
    224         * @return The week.
    225         */
    226        public int getWeek() {
    227            return this.week;
    228        }
    229
    230        /**
    231         * Returns the first millisecond of the week.  This will be determined
    232         * relative to the time zone specified in the constructor, or in the
    233         * calendar instance passed in the most recent call to the
    234         * {@link #peg(Calendar)} method.
    235         *
    236         * @return The first millisecond of the week.
    237         *
    238         * @see #getLastMillisecond()
    239         */
    240        public long getFirstMillisecond() {
    241            return this.firstMillisecond;
    242        }
    243
    244        /**
    245         * Returns the last millisecond of the week.  This will be
    246         * determined relative to the time zone specified in the constructor, or
    247         * in the calendar instance passed in the most recent call to the
    248         * {@link #peg(Calendar)} method.
    249         *
    250         * @return The last millisecond of the week.
    251         *
    252         * @see #getFirstMillisecond()
    253         */
    254        public long getLastMillisecond() {
    255            return this.lastMillisecond;
    256        }
    257
    258        /**
    259         * Recalculates the start date/time and end date/time for this time period
    260         * relative to the supplied calendar (which incorporates a time zone).
    261         *
    262         * @param calendar  the calendar (<code>null</code> not permitted).
    263         *
    264         * @since 1.0.3
    265         */
    266        public void peg(Calendar calendar) {
    267            this.firstMillisecond = getFirstMillisecond(calendar);
    268            this.lastMillisecond = getLastMillisecond(calendar);
    269        }
    270
    271        /**
    272         * Returns the week preceding this one.  This method will return
    273         * <code>null</code> for some lower limit on the range of weeks (currently
    274         * week 1, 1900).  For week 1 of any year, the previous week is always week
    275         * 53, but week 53 may not contain any days (you should check for this).
    276         *
    277         * @return The preceding week (possibly <code>null</code>).
    278         */
    279        public RegularTimePeriod previous() {
    280
    281            Week result;
    282            if (this.week != FIRST_WEEK_IN_YEAR) {
    283                result = new Week(this.week - 1, this.year);
    284            }
    285            else {
    286                // we need to work out if the previous year has 52 or 53 weeks...
    287                if (this.year > 1900) {
    288                    int yy = this.year - 1;
    289                    Calendar prevYearCalendar = Calendar.getInstance();
    290                    prevYearCalendar.set(yy, Calendar.DECEMBER, 31);
    291                    result = new Week(prevYearCalendar.getActualMaximum(
    292                            Calendar.WEEK_OF_YEAR), yy);
    293                }
    294                else {
    295                    result = null;
    296                }
    297            }
    298            return result;
    299
    300        }
    301
    302        /**
    303         * Returns the week following this one.  This method will return
    304         * <code>null</code> for some upper limit on the range of weeks (currently
    305         * week 53, 9999).  For week 52 of any year, the following week is always
    306         * week 53, but week 53 may not contain any days (you should check for
    307         * this).
    308         *
    309         * @return The following week (possibly <code>null</code>).
    310         */
    311        public RegularTimePeriod next() {
    312
    313            Week result;
    314            if (this.week < 52) {
    315                result = new Week(this.week + 1, this.year);
    316            }
    317            else {
    318                Calendar calendar = Calendar.getInstance();
    319                calendar.set(this.year, Calendar.DECEMBER, 31);
    320                int actualMaxWeek
    321                    = calendar.getActualMaximum(Calendar.WEEK_OF_YEAR);
    322                if (this.week < actualMaxWeek) {
    323                    result = new Week(this.week + 1, this.year);
    324                }
    325                else {
    326                    if (this.year < 9999) {
    327                        result = new Week(FIRST_WEEK_IN_YEAR, this.year + 1);
    328                    }
    329                    else {
    330                        result = null;
    331                    }
    332                }
    333            }
    334            return result;
    335
    336        }
    337
    338        /**
    339         * Returns a serial index number for the week.
    340         *
    341         * @return The serial index number.
    342         */
    343        public long getSerialIndex() {
    344            return this.year * 53L + this.week;
    345        }
    346
    347        /**
    348         * Returns the first millisecond of the week, evaluated using the supplied
    349         * calendar (which determines the time zone).
    350         *
    351         * @param calendar  the calendar (<code>null</code> not permitted).
    352         *
    353         * @return The first millisecond of the week.
    354         *
    355         * @throws NullPointerException if <code>calendar</code> is
    356         *     <code>null</code>.
    357         */
    358        public long getFirstMillisecond(Calendar calendar) {
    359            Calendar c = (Calendar) calendar.clone();
    360            c.clear();
    361            c.set(Calendar.YEAR, this.year);
    362            c.set(Calendar.WEEK_OF_YEAR, this.week);
    363            c.set(Calendar.DAY_OF_WEEK, c.getFirstDayOfWeek());
    364            c.set(Calendar.HOUR, 0);
    365            c.set(Calendar.MINUTE, 0);
    366            c.set(Calendar.SECOND, 0);
    367            c.set(Calendar.MILLISECOND, 0);
    368            //return c.getTimeInMillis();  // this won't work for JDK 1.3
    369            return c.getTime().getTime();
    370        }
    371
    372        /**
    373         * Returns the last millisecond of the week, evaluated using the supplied
    374         * calendar (which determines the time zone).
    375         *
    376         * @param calendar  the calendar (<code>null</code> not permitted).
    377         *
    378         * @return The last millisecond of the week.
    379         *
    380         * @throws NullPointerException if <code>calendar</code> is
    381         *     <code>null</code>.
    382         */
    383        public long getLastMillisecond(Calendar calendar) {
    384            Calendar c = (Calendar) calendar.clone();
    385            c.clear();
    386            c.set(Calendar.YEAR, this.year);
    387            c.set(Calendar.WEEK_OF_YEAR, this.week + 1);
    388            c.set(Calendar.DAY_OF_WEEK, c.getFirstDayOfWeek());
    389            c.set(Calendar.HOUR, 0);
    390            c.set(Calendar.MINUTE, 0);
    391            c.set(Calendar.SECOND, 0);
    392            c.set(Calendar.MILLISECOND, 0);
    393            //return c.getTimeInMillis();  // this won't work for JDK 1.3
    394            return c.getTime().getTime() - 1;
    395        }
    396
    397        /**
    398         * Returns a string representing the week (e.g. "Week 9, 2002").
    399         *
    400         * TODO: look at internationalisation.
    401         *
    402         * @return A string representing the week.
    403         */
    404        public String toString() {
    405            return "Week " + this.week + ", " + this.year;
    406        }
    407
    408        /**
    409         * Tests the equality of this Week object to an arbitrary object.  Returns
    410         * true if the target is a Week instance representing the same week as this
    411         * object.  In all other cases, returns false.
    412         *
    413         * @param obj  the object (<code>null</code> permitted).
    414         *
    415         * @return <code>true</code> if week and year of this and object are the
    416         *         same.
    417         */
    418        public boolean equals(Object obj) {
    419
    420            if (obj == this) {
    421                return true;
    422            }
    423            if (!(obj instanceof Week)) {
    424                return false;
    425            }
    426            Week that = (Week) obj;
    427            if (this.week != that.week) {
    428                return false;
    429            }
    430            if (this.year != that.year) {
    431                return false;
    432            }
    433            return true;
    434
    435        }
    436
    437        /**
    438         * Returns a hash code for this object instance.  The approach described by
    439         * Joshua Bloch in "Effective Java" has been used here:
    440         * <p>
    441         * <code>http://developer.java.sun.com/developer/Books/effectivejava
    442         * /Chapter3.pdf</code>
    443         *
    444         * @return A hash code.
    445         */
    446        public int hashCode() {
    447            int result = 17;
    448            result = 37 * result + this.week;
    449            result = 37 * result + this.year;
    450            return result;
    451        }
    452
    453        /**
    454         * Returns an integer indicating the order of this Week object relative to
    455         * the specified object:
    456         *
    457         * negative == before, zero == same, positive == after.
    458         *
    459         * @param o1  the object to compare.
    460         *
    461         * @return negative == before, zero == same, positive == after.
    462         */
    463        public int compareTo(Object o1) {
    464
    465            int result;
    466
    467            // CASE 1 : Comparing to another Week object
    468            // --------------------------------------------
    469            if (o1 instanceof Week) {
    470                Week w = (Week) o1;
    471                result = this.year - w.getYear().getYear();
    472                if (result == 0) {
    473                    result = this.week - w.getWeek();
    474                }
    475            }
    476
    477            // CASE 2 : Comparing to another TimePeriod object
    478            // -----------------------------------------------
    479            else if (o1 instanceof RegularTimePeriod) {
    480                // more difficult case - evaluate later...
    481                result = 0;
    482            }
    483
    484            // CASE 3 : Comparing to a non-TimePeriod object
    485            // ---------------------------------------------
    486            else {
    487                // consider time periods to be ordered after general objects
    488                result = 1;
    489            }
    490
    491            return result;
    492
    493        }
    494
    495        /**
    496         * Parses the string argument as a week.
    497         * <P>
    498         * This method is required to accept the format "YYYY-Wnn".  It will also
    499         * accept "Wnn-YYYY". Anything else, at the moment, is a bonus.
    500         *
    501         * @param s  string to parse.
    502         *
    503         * @return <code>null</code> if the string is not parseable, the week
    504         *         otherwise.
    505         */
    506        public static Week parseWeek(String s) {
    507
    508            Week result = null;
    509            if (s != null) {
    510
    511                // trim whitespace from either end of the string
    512                s = s.trim();
    513
    514                int i = Week.findSeparator(s);
    515                if (i != -1) {
    516                    String s1 = s.substring(0, i).trim();
    517                    String s2 = s.substring(i + 1, s.length()).trim();
    518
    519                    Year y = Week.evaluateAsYear(s1);
    520                    int w;
    521                    if (y != null) {
    522                        w = Week.stringToWeek(s2);
    523                        if (w == -1) {
    524                            throw new TimePeriodFormatException(
    525                                "Can't evaluate the week."
    526                            );
    527                        }
    528                        result = new Week(w, y);
    529                    }
    530                    else {
    531                        y = Week.evaluateAsYear(s2);
    532                        if (y != null) {
    533                            w = Week.stringToWeek(s1);
    534                            if (w == -1) {
    535                                throw new TimePeriodFormatException(
    536                                    "Can't evaluate the week."
    537                                );
    538                            }
    539                            result = new Week(w, y);
    540                        }
    541                        else {
    542                            throw new TimePeriodFormatException(
    543                                "Can't evaluate the year."
    544                            );
    545                        }
    546                    }
    547
    548                }
    549                else {
    550                    throw new TimePeriodFormatException(
    551                        "Could not find separator."
    552                    );
    553                }
    554
    555            }
    556            return result;
    557
    558        }
    559
    560        /**
    561         * Finds the first occurrence of ' ', '-', ',' or '.'
    562         *
    563         * @param s  the string to parse.
    564         *
    565         * @return <code>-1</code> if none of the characters was found, the
    566         *      index of the first occurrence otherwise.
    567         */
    568        private static int findSeparator(String s) {
    569
    570            int result = s.indexOf('-');
    571            if (result == -1) {
    572                result = s.indexOf(',');
    573            }
    574            if (result == -1) {
    575                result = s.indexOf(' ');
    576            }
    577            if (result == -1) {
    578                result = s.indexOf('.');
    579            }
    580            return result;
    581        }
    582
    583        /**
    584         * Creates a year from a string, or returns null (format exceptions
    585         * suppressed).
    586         *
    587         * @param s  string to parse.
    588         *
    589         * @return <code>null</code> if the string is not parseable, the year
    590         *         otherwise.
    591         */
    592        private static Year evaluateAsYear(String s) {
    593
    594            Year result = null;
    595            try {
    596                result = Year.parseYear(s);
    597            }
    598            catch (TimePeriodFormatException e) {
    599                // suppress
    600            }
    601            return result;
    602
    603        }
    604
    605        /**
    606         * Converts a string to a week.
    607         *
    608         * @param s  the string to parse.
    609         * @return <code>-1</code> if the string does not contain a week number,
    610         *         the number of the week otherwise.
    611         */
    612        private static int stringToWeek(String s) {
    613
    614            int result = -1;
    615            s = s.replace('W', ' ');
    616            s = s.trim();
    617            try {
    618                result = Integer.parseInt(s);
    619                if ((result < 1) || (result > LAST_WEEK_IN_YEAR)) {
    620                    result = -1;
    621                }
    622            }
    623            catch (NumberFormatException e) {
    624                // suppress
    625            }
    626            return result;
    627
    628        }
    629
    630    }