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     * Month.java
029     * ----------
030     * (C) Copyright 2001-2005, 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.2 2005/11/02 13:20:29 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     *
057     */
058    
059    package org.jfree.data.time;
060    
061    import java.io.Serializable;
062    import java.util.Calendar;
063    import java.util.Date;
064    import java.util.TimeZone;
065    
066    import org.jfree.date.MonthConstants;
067    import org.jfree.date.SerialDate;
068    
069    /**
070     * Represents a single month.  This class is immutable, which is a requirement
071     * for all {@link RegularTimePeriod} subclasses.
072     */
073    public class Month extends RegularTimePeriod implements Serializable {
074    
075        /** For serialization. */
076        private static final long serialVersionUID = -5090216912548722570L;
077    
078        /** The month (1-12). */
079        private int month;
080    
081        /** The year in which the month falls. */
082        private Year year;
083    
084        /**
085         * Constructs a new Month, based on the current system time.
086         */
087        public Month() {
088            this(new Date());
089        }
090    
091        /**
092         * Constructs a new month instance.
093         *
094         * @param month  the month (in the range 1 to 12).
095         * @param year  the year.
096         */
097        public Month(int month, int year) {
098            this(month, new Year(year));
099        }
100    
101        /**
102         * Constructs a new month instance.
103         *
104         * @param month  the month (in the range 1 to 12).
105         * @param year  the year.
106         */
107        public Month(int month, Year year) {
108    
109            if ((month < 1) || (month > 12)) {
110                throw new IllegalArgumentException("Month outside valid range.");
111            }
112            this.month = month;
113            this.year = year;
114    
115        }
116    
117        /**
118         * Constructs a new month instance, based on a date/time and the default 
119         * time zone.
120         *
121         * @param time  the date/time.
122         */
123        public Month(Date time) {
124            this(time, RegularTimePeriod.DEFAULT_TIME_ZONE);
125        }
126    
127        /**
128         * Constructs a Month, based on a date/time and a time zone.
129         *
130         * @param time  the date/time.
131         * @param zone  the time zone.
132         */
133        public Month(Date time, TimeZone zone) {
134            Calendar calendar = Calendar.getInstance(zone);
135            calendar.setTime(time);
136            this.month = calendar.get(Calendar.MONTH) + 1;
137            this.year = new Year(calendar.get(Calendar.YEAR));
138        }
139    
140        /**
141         * Returns the year in which the month falls.
142         *
143         * @return The year in which the month falls (as a Year object).
144         */
145        public Year getYear() {
146            return this.year;
147        }
148    
149        /**
150         * Returns the year in which the month falls.
151         *
152         * @return The year in which the monht falls (as an int).
153         */
154        public int getYearValue() {
155            return this.year.getYear();
156        }
157    
158        /**
159         * Returns the month.  Note that 1=JAN, 2=FEB, ...
160         *
161         * @return The month.
162         */
163        public int getMonth() {
164            return this.month;
165        }
166    
167        /**
168         * Returns the month preceding this one.
169         *
170         * @return The month preceding this one.
171         */
172        public RegularTimePeriod previous() {
173    
174            Month result;
175            if (this.month != MonthConstants.JANUARY) {
176                result = new Month(this.month - 1, this.year);
177            }
178            else {
179                Year prevYear = (Year) this.year.previous();
180                if (prevYear != null) {
181                    result = new Month(MonthConstants.DECEMBER, prevYear);
182                }
183                else {
184                    result = null;
185                }
186            }
187            return result;
188    
189        }
190    
191        /**
192         * Returns the month following this one.
193         *
194         * @return The month following this one.
195         */
196        public RegularTimePeriod next() {
197            Month result;
198            if (this.month != MonthConstants.DECEMBER) {
199                result = new Month(this.month + 1, this.year);
200            }
201            else {
202                Year nextYear = (Year) this.year.next();
203                if (nextYear != null) {
204                    result = new Month(MonthConstants.JANUARY, nextYear);
205                }
206                else {
207                    result = null;
208                }
209            }
210            return result;
211        }
212    
213        /**
214         * Returns a serial index number for the month.
215         *
216         * @return The serial index number.
217         */
218        public long getSerialIndex() {
219            return this.year.getYear() * 12L + this.month;
220        }
221    
222        /**
223         * Returns a string representing the month (e.g. "January 2002").
224         * <P>
225         * To do: look at internationalisation.
226         *
227         * @return A string representing the month.
228         */
229        public String toString() {
230            return SerialDate.monthCodeToString(this.month) + " " + this.year;
231        }
232    
233        /**
234         * Tests the equality of this Month object to an arbitrary object.
235         * Returns true if the target is a Month instance representing the same
236         * month as this object.  In all other cases, returns false.
237         *
238         * @param obj  the object.
239         *
240         * @return <code>true</code> if month and year of this and object are the 
241         *         same.
242         */
243        public boolean equals(Object obj) {
244    
245            if (obj != null) {
246                if (obj instanceof Month) {
247                    Month target = (Month) obj;
248                    return (
249                        (this.month == target.getMonth()) 
250                            && (this.year.equals(target.getYear()))
251                    );
252                }
253                else {
254                    return false;
255                }
256            }
257            else {
258                return false;
259            }
260    
261        }
262    
263        /**
264         * Returns a hash code for this object instance.  The approach described by
265         * Joshua Bloch in "Effective Java" has been used here:
266         * <p>
267         * <code>http://developer.java.sun.com/developer/Books/effectivejava
268         * /Chapter3.pdf</code>
269         * 
270         * @return A hash code.
271         */
272        public int hashCode() {
273            int result = 17;
274            result = 37 * result + this.month;
275            result = 37 * result + this.year.hashCode();
276            return result;
277        }
278    
279        /**
280         * Returns an integer indicating the order of this Month object relative to
281         * the specified
282         * object: negative == before, zero == same, positive == after.
283         *
284         * @param o1  the object to compare.
285         *
286         * @return negative == before, zero == same, positive == after.
287         */
288        public int compareTo(Object o1) {
289    
290            int result;
291    
292            // CASE 1 : Comparing to another Month object
293            // --------------------------------------------
294            if (o1 instanceof Month) {
295                Month m = (Month) o1;
296                result = this.year.getYear() - m.getYear().getYear();
297                if (result == 0) {
298                    result = this.month - m.getMonth();
299                }
300            }
301    
302            // CASE 2 : Comparing to another TimePeriod object
303            // -----------------------------------------------
304            else if (o1 instanceof RegularTimePeriod) {
305                // more difficult case - evaluate later...
306                result = 0;
307            }
308    
309            // CASE 3 : Comparing to a non-TimePeriod object
310            // ---------------------------------------------
311            else {
312                // consider time periods to be ordered after general objects
313                result = 1;
314            }
315    
316            return result;
317    
318        }
319    
320        /**
321         * Returns the first millisecond of the month, evaluated using the supplied
322         * calendar (which determines the time zone).
323         *
324         * @param calendar  the calendar.
325         *
326         * @return The first millisecond of the month.
327         */
328        public long getFirstMillisecond(Calendar calendar) {
329            Day first = new Day(1, this.month, this.year.getYear());
330            return first.getFirstMillisecond(calendar);
331        }
332    
333        /**
334         * Returns the last millisecond of the month, evaluated using the supplied
335         * calendar (which determines the time zone).
336         *
337         * @param calendar  the calendar.
338         *
339         * @return The last millisecond of the month.
340         */
341        public long getLastMillisecond(Calendar calendar) {
342            int eom = SerialDate.lastDayOfMonth(this.month, this.year.getYear());
343            Day last = new Day(eom, this.month, this.year.getYear());
344            return last.getLastMillisecond(calendar);
345        }
346    
347        /**
348         * Parses the string argument as a month.
349         * <P>
350         * This method is required to accept the format "YYYY-MM".  It will also
351         * accept "MM-YYYY". Anything else, at the moment, is a bonus.
352         *
353         * @param s  the string to parse.
354         *
355         * @return <code>null</code> if the string is not parseable, the month 
356         *         otherwise.
357         */
358        public static Month parseMonth(String s) {
359    
360            Month result = null;
361            if (s != null) {
362    
363                // trim whitespace from either end of the string
364                s = s.trim();
365    
366                int i = Month.findSeparator(s);
367                if (i != -1) {
368                    String s1 = s.substring(0, i).trim();
369                    String s2 = s.substring(i + 1, s.length()).trim();
370    
371                    Year year = Month.evaluateAsYear(s1);
372                    int month;
373                    if (year != null) {
374                        month = SerialDate.stringToMonthCode(s2);
375                        if (month == -1) {
376                            throw new TimePeriodFormatException(
377                                "Can't evaluate the month."
378                            );
379                        }
380                        result = new Month(month, year);
381                    }
382                    else {
383                        year = Month.evaluateAsYear(s2);
384                        if (year != null) {
385                            month = SerialDate.stringToMonthCode(s1);
386                            if (month == -1) {
387                                throw new TimePeriodFormatException(
388                                    "Can't evaluate the month."
389                                );
390                            }
391                            result = new Month(month, year);
392                        }
393                        else {
394                            throw new TimePeriodFormatException(
395                                "Can't evaluate the year."
396                            );
397                        }
398                    }
399    
400                }
401                else {
402                    throw new TimePeriodFormatException(
403                        "Could not find separator."
404                    );
405                }
406    
407            }
408            return result;
409    
410        }
411    
412        /**
413         * Finds the first occurrence of ' ', '-', ',' or '.'
414         *
415         * @param s  the string to parse.
416         * 
417         * @return <code>-1</code> if none of the characters where found, the
418         *      position of the first occurence otherwise.
419         */
420        private static int findSeparator(String s) {
421    
422            int result = s.indexOf('-');
423            if (result == -1) {
424                result = s.indexOf(',');
425            }
426            if (result == -1) {
427                result = s.indexOf(' ');
428            }
429            if (result == -1) {
430                result = s.indexOf('.');
431            }
432            return result;
433        }
434    
435        /**
436         * Creates a year from a string, or returns null (format exceptions 
437         * suppressed).
438         *
439         * @param s  the string to parse.
440         *
441         * @return <code>null</code> if the string is not parseable, the year 
442         *         otherwise.
443         */
444        private static Year evaluateAsYear(String s) {
445    
446            Year result = null;
447            try {
448                result = Year.parseYear(s);
449            }
450            catch (TimePeriodFormatException e) {
451                // suppress
452            }
453            return result;
454    
455        }
456    
457    }