    001    /* ===========================================================
    002     * JFreeChart : a free chart library for the Java(tm) platform
    003     * ===========================================================
    004     *
    005     * (C) Copyright 2000-2005, 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-2004, 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.1 2005/10/25 21:35:24 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     *
    063     */
    064
    065    package org.jfree.data.time;
    066
    067    import java.io.Serializable;
    068    import java.util.Calendar;
    069    import java.util.Date;
    070    import java.util.TimeZone;
    071
    072    /**
    073     * A calendar week.  All years are considered to have 53 weeks, numbered from 1
    074     * to 53, although in many cases the 53rd week is empty.  Most of the time, the
    075     * 1st week of the year *begins* in the previous calendar year, but it always
    076     * finishes in the current year (this behaviour matches the workings of the
    077     * <code>GregorianCalendar</code> class).
    078     * <P>
    079     * This class is immutable, which is a requirement for all
    080     * {@link RegularTimePeriod} subclasses.
    081     */
    082    public class Week extends RegularTimePeriod implements Serializable {
    083
    084        /** For serialization. */
    085        private static final long serialVersionUID = 1856387786939865061L;
    086
    087        /** Constant for the first week in the year. */
    088        public static final int FIRST_WEEK_IN_YEAR = 1;
    089
    090        /** Constant for the last week in the year. */
    091        public static final int LAST_WEEK_IN_YEAR = 53;
    092
    093        /** The year in which the week falls. */
    094        private Year year;
    095
    096        /** The week (1-53). */
    097        private int week;
    098
    099        /**
    100         * Creates a new time period for the week in which the current system
    101         * date/time falls.
    102         */
    103        public Week() {
    104            this(new Date());
    105        }
    106
    107        /**
    108         * Creates a time period representing the week in the specified year.
    109         *
    110         * @param week  the week (1 to 53).
    111         * @param year  the year (1900 to 9999).
    112         */
    113        public Week(int week, int year) {
    114            this(week, new Year(year));
    115        }
    116
    117        /**
    118         * Creates a time period representing the week in the specified year.
    119         *
    120         * @param week  the week (1 to 53).
    121         * @param year  the year (1900 to 9999).
    122         */
    123        public Week(int week, Year year) {
    124            if ((week < FIRST_WEEK_IN_YEAR) && (week > LAST_WEEK_IN_YEAR)) {
    125                throw new IllegalArgumentException(
    126                    "The 'week' argument must be in the range 1 - 53."
    127                );
    128            }
    129            this.week = week;
    130            this.year = year;
    131        }
    132
    133        /**
    134         * Creates a time period for the week in which the specified date/time
    135         * falls.
    136         *
    137         * @param time  the time (<code>null</code> not permitted).
    138         */
    139        public Week(Date time) {
    140            // defer argument checking...
    141            this(time, RegularTimePeriod.DEFAULT_TIME_ZONE);
    142        }
    143
    144        /**
    145         * Creates a time period for the week in which the specified date/time
    146         * falls, calculated relative to the specified time zone.
    147         *
    148         * @param time  the date/time (<code>null</code> not permitted).
    149         * @param zone  the time zone (<code>null</code> not permitted).
    150         */
    151        public Week(Date time, TimeZone zone) {
    152            if (time == null) {
    153                throw new IllegalArgumentException("Null 'time' argument.");
    154            }
    155            if (zone == null) {
    156                throw new IllegalArgumentException("Null 'zone' argument.");
    157            }
    158            Calendar calendar = Calendar.getInstance(zone);
    159            calendar.setTime(time);
    160
    161            // sometimes the last few days of the year are considered to fall in
    162            // the *first* week of the following year.  Refer to the Javadocs for
    163            // GregorianCalendar.
    164            int tempWeek = calendar.get(Calendar.WEEK_OF_YEAR);
    165            if (tempWeek == 1
    166                    && calendar.get(Calendar.MONTH) == Calendar.DECEMBER) {
    167                this.week = 1;
    168                this.year =  new Year(calendar.get(Calendar.YEAR) + 1);
    169            }
    170            else {
    171                this.week = Math.min(tempWeek, LAST_WEEK_IN_YEAR);
    172                this.year = new Year(calendar.get(Calendar.YEAR));
    173            }
    174
    175        }
    176
    177        /**
    178         * Returns the year in which the week falls.
    179         *
    180         * @return The year (never <code>null</code>).
    181         */
    182        public Year getYear() {
    183            return this.year;
    184        }
    185
    186        /**
    187         * Returns the year in which the week falls, as an integer value.
    188         *
    189         * @return The year.
    190         */
    191        public int getYearValue() {
    192            return this.year.getYear();
    193        }
    194
    195        /**
    196         * Returns the week.
    197         *
    198         * @return The week.
    199         */
    200        public int getWeek() {
    201            return this.week;
    202        }
    203
    204        /**
    205         * Returns the week preceding this one.  This method will return
    206         * <code>null</code> for some lower limit on the range of weeks (currently
    207         * week 1, 1900).  For week 1 of any year, the previous week is always week
    208         * 53, but week 53 may not contain any days (you should check for this).
    209         *
    210         * @return The preceding week (possibly <code>null</code>).
    211         */
    212        public RegularTimePeriod previous() {
    213
    214            Week result;
    215            if (this.week != FIRST_WEEK_IN_YEAR) {
    216                result = new Week(this.week - 1, this.year);
    217            }
    218            else {
    219                // we need to work out if the previous year has 52 or 53 weeks...
    220                Year prevYear = (Year) this.year.previous();
    221                if (prevYear != null) {
    222                    int yy = prevYear.getYear();
    223                    Calendar prevYearCalendar = Calendar.getInstance();
    224                    prevYearCalendar.set(yy, Calendar.DECEMBER, 31);
    225                    result = new Week(
    226                        prevYearCalendar.getActualMaximum(Calendar.WEEK_OF_YEAR),
    227                        prevYear
    228                    );
    229                }
    230                else {
    231                    result = null;
    232                }
    233            }
    234            return result;
    235
    236        }
    237
    238        /**
    239         * Returns the week following this one.  This method will return
    240         * <code>null</code> for some upper limit on the range of weeks (currently
    241         * week 53, 9999).  For week 52 of any year, the following week is always
    242         * week 53, but week 53 may not contain any days (you should check for
    243         * this).
    244         *
    245         * @return The following week (possibly <code>null</code>).
    246         */
    247        public RegularTimePeriod next() {
    248
    249            Week result;
    250            if (this.week < 52) {
    251                result = new Week(this.week + 1, this.year);
    252            }
    253            else {
    254                Calendar calendar = Calendar.getInstance();
    255                calendar.set(this.year.getYear(), Calendar.DECEMBER, 31);
    256                int actualMaxWeek
    257                    = calendar.getActualMaximum(Calendar.WEEK_OF_YEAR);
    258                if (this.week != actualMaxWeek) {
    259                    result = new Week(this.week + 1, this.year);
    260                }
    261                else {
    262                    Year nextYear = (Year) this.year.next();
    263                    if (nextYear != null) {
    264                        result = new Week(FIRST_WEEK_IN_YEAR, nextYear);
    265                    }
    266                    else {
    267                        result = null;
    268                    }
    269                }
    270            }
    271            return result;
    272
    273        }
    274
    275        /**
    276         * Returns a serial index number for the week.
    277         *
    278         * @return The serial index number.
    279         */
    280        public long getSerialIndex() {
    281            return this.year.getYear() * 53L + this.week;
    282        }
    283
    284        /**
    285         * Returns the first millisecond of the week, evaluated using the supplied
    286         * calendar (which determines the time zone).
    287         *
    288         * @param calendar  the calendar.
    289         *
    290         * @return The first millisecond of the week.
    291         */
    292        public long getFirstMillisecond(Calendar calendar) {
    293            Calendar c = (Calendar) calendar.clone();
    294            c.clear();
    295            c.set(Calendar.YEAR, this.year.getYear());
    296            c.set(Calendar.WEEK_OF_YEAR, this.week);
    297            c.set(Calendar.DAY_OF_WEEK, c.getFirstDayOfWeek());
    298            c.set(Calendar.HOUR, 0);
    299            c.set(Calendar.MINUTE, 0);
    300            c.set(Calendar.SECOND, 0);
    301            c.set(Calendar.MILLISECOND, 0);
    302            //return c.getTimeInMillis();  // this won't work for JDK 1.3
    303            return c.getTime().getTime();
    304        }
    305
    306        /**
    307         * Returns the last millisecond of the week, evaluated using the supplied
    308         * calendar (which determines the time zone).
    309         *
    310         * @param calendar  the calendar.
    311         *
    312         * @return The last millisecond of the week.
    313         */
    314        public long getLastMillisecond(Calendar calendar) {
    315            RegularTimePeriod next = next();
    316            return next.getFirstMillisecond(calendar) - 1;
    317        }
    318
    319        /**
    320         * Returns a string representing the week (e.g. "Week 9, 2002").
    321         *
    322         * TODO: look at internationalisation.
    323         *
    324         * @return A string representing the week.
    325         */
    326        public String toString() {
    327            return "Week " + this.week + ", " + this.year;
    328        }
    329
    330        /**
    331         * Tests the equality of this Week object to an arbitrary object.  Returns
    332         * true if the target is a Week instance representing the same week as this
    333         * object.  In all other cases, returns false.
    334         * @param obj The object.
    335         *
    336         * @return <code>true</code> if week and year of this and object are the
    337         *         same.
    338         */
    339        public boolean equals(Object obj) {
    340
    341            if (obj == this) {
    342                return true;
    343            }
    344            if (!(obj instanceof Week)) {
    345                return false;
    346            }
    347            Week that = (Week) obj;
    348            if (this.week != that.week) {
    349                return false;
    350            }
    351            if (!this.year.equals(that.year)) {
    352                return false;
    353            }
    354            return true;
    355
    356        }
    357
    358        /**
    359         * Returns a hash code for this object instance.  The approach described by
    360         * Joshua Bloch in "Effective Java" has been used here:
    361         * <p>
    362         * <code>http://developer.java.sun.com/developer/Books/effectivejava
    363         * /Chapter3.pdf</code>
    364         *
    365         * @return A hash code.
    366         */
    367        public int hashCode() {
    368            int result = 17;
    369            result = 37 * result + this.week;
    370            result = 37 * result + this.year.hashCode();
    371            return result;
    372        }
    373
    374        /**
    375         * Returns an integer indicating the order of this Week object relative to
    376         * the specified object:
    377         *
    378         * negative == before, zero == same, positive == after.
    379         *
    380         * @param o1  the object to compare.
    381         *
    382         * @return negative == before, zero == same, positive == after.
    383         */
    384        public int compareTo(Object o1) {
    385
    386            int result;
    387
    388            // CASE 1 : Comparing to another Week object
    389            // --------------------------------------------
    390            if (o1 instanceof Week) {
    391                Week w = (Week) o1;
    392                result = this.year.getYear() - w.getYear().getYear();
    393                if (result == 0) {
    394                    result = this.week - w.getWeek();
    395                }
    396            }
    397
    398            // CASE 2 : Comparing to another TimePeriod object
    399            // -----------------------------------------------
    400            else if (o1 instanceof RegularTimePeriod) {
    401                // more difficult case - evaluate later...
    402                result = 0;
    403            }
    404
    405            // CASE 3 : Comparing to a non-TimePeriod object
    406            // ---------------------------------------------
    407            else {
    408                // consider time periods to be ordered after general objects
    409                result = 1;
    410            }
    411
    412            return result;
    413
    414        }
    415
    416        /**
    417         * Parses the string argument as a week.
    418         * <P>
    419         * This method is required to accept the format "YYYY-Wnn".  It will also
    420         * accept "Wnn-YYYY". Anything else, at the moment, is a bonus.
    421         *
    422         * @param s  string to parse.
    423         *
    424         * @return <code>null</code> if the string is not parseable, the week
    425         *         otherwise.
    426         */
    427        public static Week parseWeek(String s) {
    428
    429            Week result = null;
    430            if (s != null) {
    431
    432                // trim whitespace from either end of the string
    433                s = s.trim();
    434
    435                int i = Week.findSeparator(s);
    436                if (i != -1) {
    437                    String s1 = s.substring(0, i).trim();
    438                    String s2 = s.substring(i + 1, s.length()).trim();
    439
    440                    Year y = Week.evaluateAsYear(s1);
    441                    int w;
    442                    if (y != null) {
    443                        w = Week.stringToWeek(s2);
    444                        if (w == -1) {
    445                            throw new TimePeriodFormatException(
    446                                "Can't evaluate the week."
    447                            );
    448                        }
    449                        result = new Week(w, y);
    450                    }
    451                    else {
    452                        y = Week.evaluateAsYear(s2);
    453                        if (y != null) {
    454                            w = Week.stringToWeek(s1);
    455                            if (w == -1) {
    456                                throw new TimePeriodFormatException(
    457                                    "Can't evaluate the week."
    458                                );
    459                            }
    460                            result = new Week(w, y);
    461                        }
    462                        else {
    463                            throw new TimePeriodFormatException(
    464                                "Can't evaluate the year."
    465                            );
    466                        }
    467                    }
    468
    469                }
    470                else {
    471                    throw new TimePeriodFormatException(
    472                        "Could not find separator."
    473                    );
    474                }
    475
    476            }
    477            return result;
    478
    479        }
    480
    481        /**
    482         * Finds the first occurrence of ' ', '-', ',' or '.'
    483         *
    484         * @param s  the string to parse.
    485         *
    486         * @return <code>-1</code> if none of the characters was found, the
    487         *      index of the first occurrence otherwise.
    488         */
    489        private static int findSeparator(String s) {
    490
    491            int result = s.indexOf('-');
    492            if (result == -1) {
    493                result = s.indexOf(',');
    494            }
    495            if (result == -1) {
    496                result = s.indexOf(' ');
    497            }
    498            if (result == -1) {
    499                result = s.indexOf('.');
    500            }
    501            return result;
    502        }
    503
    504        /**
    505         * Creates a year from a string, or returns null (format exceptions
    506         * suppressed).
    507         *
    508         * @param s  string to parse.
    509         *
    510         * @return <code>null</code> if the string is not parseable, the year
    511         *         otherwise.
    512         */
    513        private static Year evaluateAsYear(String s) {
    514
    515            Year result = null;
    516            try {
    517                result = Year.parseYear(s);
    518            }
    519            catch (TimePeriodFormatException e) {
    520                // suppress
    521            }
    522            return result;
    523
    524        }
    525
    526        /**
    527         * Converts a string to a week.
    528         *
    529         * @param s  the string to parse.
    530         * @return <code>-1</code> if the string does not contain a week number,
    531         *         the number of the week otherwise.
    532         */
    533        private static int stringToWeek(String s) {
    534
    535            int result = -1;
    536            s = s.replace('W', ' ');
    537            s = s.trim();
    538            try {
    539                result = Integer.parseInt(s);
    540                if ((result < 1) || (result > LAST_WEEK_IN_YEAR)) {
    541                    result = -1;
    542                }
    543            }
    544            catch (NumberFormatException e) {
    545                // suppress
    546            }
    547            return result;
    548
    549        }
    550
    551    }