001package biweekly.util;
002
003import java.io.Serializable;
004import java.text.DecimalFormat;
005import java.text.NumberFormat;
006import java.util.Calendar;
007import java.util.Date;
008import java.util.TimeZone;
009import java.util.regex.Matcher;
010import java.util.regex.Pattern;
011
012/*
013 Copyright (c) 2013-2015, Michael Angstadt
014 All rights reserved.
015
016 Redistribution and use in source and binary forms, with or without
017 modification, are permitted provided that the following conditions are met: 
018
019 1. Redistributions of source code must retain the above copyright notice, this
020 list of conditions and the following disclaimer. 
021 2. Redistributions in binary form must reproduce the above copyright notice,
022 this list of conditions and the following disclaimer in the documentation
023 and/or other materials provided with the distribution. 
024
025 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
026 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
027 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
028 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
029 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
030 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
031 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
032 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
033 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
034 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
035 */
036
037/**
038 * <p>
039 * Contains the raw components of a date-time value.
040 * </p>
041 * <p>
042 * <b>Examples:</b>
043 * 
044 * <pre class="brush:java">
045 * //July 22, 2013 at 17:25
046 * DateTimeComponents components = new DateTimeComponents(2013, 07, 22, 17, 25, 0, false);
047 * 
048 * //parsing a date string (accepts basic and extended formats)
049 * DateTimeComponents components = DateTimeComponents.parse(&quot;20130722T172500&quot;);
050 * 
051 * //converting to date string
052 * DateTimeComponents components = new DateTimeComponents(2013, 07, 22, 17, 25, 0, false);
053 * String str = components.toString(true); //&quot;2013-07-22T17:25:00&quot;
054 * 
055 * //converting to a Date object
056 * DateTimeComponents components = new DateTimeComponents(2013, 07, 22, 17, 25, 0, false);
057 * Date date = components.toDate();
058 * 
059 * </pre>
060 * 
061 * </p>
062 * @author Michael Angstadt
063 */
064public final class DateTimeComponents implements Comparable<DateTimeComponents>, Serializable {
065        private static final long serialVersionUID = 7668029303206402368L;
066        private static final Pattern regex = Pattern.compile("^(\\d{4})-?(\\d{2})-?(\\d{2})(T(\\d{2}):?(\\d{2}):?(\\d{2})(Z?))?.*");
067        private final int year, month, date, hour, minute, second;
068        private final boolean hasTime, utc;
069
070        /**
071         * Parses the components out of a date-time string.
072         * @param dateString the date-time string (basic and extended formats are
073         * supported, e.g. "20130331T020000" or "2013-03-31T02:00:00")
074         * @return the parsed components
075         * @throws IllegalArgumentException if the date string cannot be parsed
076         */
077        public static DateTimeComponents parse(String dateString) {
078                return parse(dateString, null);
079        }
080
081        /**
082         * Parses the components out of a date-time string.
083         * @param dateString the date-time string (basic and extended formats are
084         * supported, e.g. "20130331T020000" or "2013-03-31T02:00:00")
085         * @param hasTime true to force the value to be parsed as a date-time value,
086         * false to force the value to be parsed as a date value, null to parse the
087         * value however it is
088         * @return the parsed components
089         * @throws IllegalArgumentException if the date string cannot be parsed
090         */
091        public static DateTimeComponents parse(String dateString, Boolean hasTime) {
092                Matcher m = regex.matcher(dateString);
093                if (!m.find()) {
094                        throw new IllegalArgumentException("Cannot parse date: " + dateString);
095                }
096
097                int i = 1;
098                int year = Integer.parseInt(m.group(i++));
099                int month = Integer.parseInt(m.group(i++));
100                int date = Integer.parseInt(m.group(i++));
101
102                i++; //skip
103
104                String hourStr = m.group(i++);
105                if (hasTime == null) {
106                        hasTime = (hourStr != null);
107                }
108                if (!hasTime) {
109                        return new DateTimeComponents(year, month, date);
110                }
111
112                int hour = (hourStr == null) ? 0 : Integer.parseInt(hourStr);
113
114                String minuteStr = m.group(i++);
115                int minute = (minuteStr == null) ? 0 : Integer.parseInt(minuteStr);
116
117                String secondStr = m.group(i++);
118                int second = (secondStr == null) ? 0 : Integer.parseInt(secondStr);
119
120                boolean utc = "Z".equals(m.group(i++));
121
122                return new DateTimeComponents(year, month, date, hour, minute, second, utc);
123        }
124
125        /**
126         * Copies an existing DateTimeComponents object.
127         * @param original the object to copy from
128         */
129        public DateTimeComponents(DateTimeComponents original) {
130                this(original, null, null, null, null, null, null, null);
131        }
132
133        /**
134         * Copies an existing DateTimeComponents object.
135         * @param original the object to copy from
136         * @param year the new year value or null not to change
137         * @param month the new month value or null not to change
138         * @param date the new date value or null not to change
139         * @param hour the new hour value or null not to change
140         * @param minute the new minute value or null not to change
141         * @param second the new second value or null not to change
142         * @param utc true if the time is in UTC, false if not, or null not to
143         * change
144         */
145        public DateTimeComponents(DateTimeComponents original, Integer year, Integer month, Integer date, Integer hour, Integer minute, Integer second, Boolean utc) {
146                //@formatter:off
147                this(
148                        (year == null) ? original.year : year,
149                        (month == null) ? original.month : month,
150                        (date == null) ? original.date : date,
151                        (hour == null) ? original.hour : hour,
152                        (minute == null) ? original.minute : minute,
153                        (second == null) ? original.second : second,
154                        (utc == null) ? original.utc : utc
155                );
156                //@formatter:on
157        }
158
159        /**
160         * Creates a set of date components.
161         * @param year the year (e.g. "2013")
162         * @param month the month (e.g. "1" for January)
163         * @param date the date of the month (e.g. "15")
164         */
165        public DateTimeComponents(int year, int month, int date) {
166                this(year, month, date, 0, 0, 0, false, false);
167        }
168
169        /**
170         * Creates a set of date-time components.
171         * @param year the year (e.g. "2013")
172         * @param month the month (e.g. "1" for January)
173         * @param date the date of the month (e.g. "15")
174         * @param hour the hour (e.g. "13")
175         * @param minute the minute
176         * @param second the second
177         * @param utc true if the time is in UTC, false if not
178         */
179        public DateTimeComponents(int year, int month, int date, int hour, int minute, int second, boolean utc) {
180                this(year, month, date, hour, minute, second, utc, true);
181        }
182
183        private DateTimeComponents(int year, int month, int date, int hour, int minute, int second, boolean utc, boolean hasTime) {
184                this.year = year;
185                this.month = month;
186                this.date = date;
187                this.hour = hour;
188                this.minute = minute;
189                this.second = second;
190                this.utc = utc;
191                this.hasTime = hasTime;
192        }
193
194        /**
195         * Creates a set of date-time components in the local timezone from a
196         * {@link Date} object.
197         * @param date the date object
198         */
199        public DateTimeComponents(Date date) {
200                this(date, TimeZone.getDefault());
201        }
202
203        /**
204         * Creates a set of date-time components from a {@link Date} object.
205         * @param date the date object
206         * @param timezone the timezone the date-time components will be in
207         */
208        public DateTimeComponents(Date date, TimeZone timezone) {
209                Calendar cal = Calendar.getInstance(timezone);
210                cal.setTime(date);
211
212                year = cal.get(Calendar.YEAR);
213                month = cal.get(Calendar.MONTH) + 1;
214                this.date = cal.get(Calendar.DATE);
215                hour = cal.get(Calendar.HOUR_OF_DAY);
216                minute = cal.get(Calendar.MINUTE);
217                second = cal.get(Calendar.SECOND);
218                utc = false;
219                hasTime = true;
220        }
221
222        /**
223         * Gets the year component.
224         * @return the year
225         */
226        public int getYear() {
227                return year;
228        }
229
230        /**
231         * Gets the month component.
232         * @return the month (e.g. "1" for January)
233         */
234        public int getMonth() {
235                return month;
236        }
237
238        /**
239         * Gets the date component
240         * @return the date
241         */
242        public int getDate() {
243                return date;
244        }
245
246        /**
247         * Gets whether these components contain a time component
248         * @return true if it has a time component, false if it's strictly a date
249         */
250        public boolean hasTime() {
251                return hasTime;
252        }
253
254        /**
255         * Gets the hour component
256         * @return the hour
257         */
258        public int getHour() {
259                return hour;
260        }
261
262        /**
263         * Gets the minute component.
264         * @return the minute
265         */
266        public int getMinute() {
267                return minute;
268        }
269
270        /**
271         * Gets the second component.
272         * @return the second
273         */
274        public int getSecond() {
275                return second;
276        }
277
278        /**
279         * Gets whether the time is in UTC or not
280         * @return true if the time is in UTC, false if not
281         */
282        public boolean isUtc() {
283                return utc;
284        }
285
286        /**
287         * Converts the date-time components to a string using "basic" format.
288         * @return the date string
289         */
290        @Override
291        public String toString() {
292                return toString(true, false);
293        }
294
295        /**
296         * Converts the date-time components to a string.
297         * @param includeTime true to include the time portion, false not to
298         * @param extended true to use extended format, false to use basic
299         * @return the date string
300         */
301        public String toString(boolean includeTime, boolean extended) {
302                NumberFormat nf = new DecimalFormat("00");
303                String dash = extended ? "-" : "";
304                String colon = extended ? ":" : "";
305                String z = utc ? "Z" : "";
306
307                StringBuilder sb = new StringBuilder();
308                sb.append(year).append(dash).append(nf.format(month)).append(dash).append(nf.format(date));
309                if (includeTime) {
310                        sb.append("T").append(nf.format(hour)).append(colon).append(nf.format(minute)).append(colon).append(nf.format(second)).append(z);
311                }
312                return sb.toString();
313        }
314
315        /**
316         * Converts the date-time components to a {@link Date} object.
317         * @return the date object
318         */
319        public Date toDate() {
320                TimeZone timezone = utc ? TimeZone.getTimeZone("UTC") : TimeZone.getDefault();
321                return toDate(timezone);
322        }
323
324        /**
325         * Converts the date-time components to a {@link Date} object.
326         * @return the date object
327         */
328        public Date toDate(TimeZone timezone) {
329                Calendar c = Calendar.getInstance(timezone);
330                c.clear();
331                c.set(Calendar.YEAR, year);
332                c.set(Calendar.MONTH, month - 1);
333                c.set(Calendar.DATE, date);
334                c.set(Calendar.HOUR_OF_DAY, hour);
335                c.set(Calendar.MINUTE, minute);
336                c.set(Calendar.SECOND, second);
337                return c.getTime();
338        }
339
340        @Override
341        public int hashCode() {
342                final int prime = 31;
343                int result = 1;
344                result = prime * result + date;
345                result = prime * result + (hasTime ? 1231 : 1237);
346                result = prime * result + hour;
347                result = prime * result + minute;
348                result = prime * result + month;
349                result = prime * result + second;
350                result = prime * result + (utc ? 1231 : 1237);
351                result = prime * result + year;
352                return result;
353        }
354
355        @Override
356        public boolean equals(Object obj) {
357                if (this == obj) return true;
358                if (obj == null) return false;
359                if (getClass() != obj.getClass()) return false;
360                DateTimeComponents other = (DateTimeComponents) obj;
361                if (date != other.date) return false;
362                if (hasTime != other.hasTime) return false;
363                if (hour != other.hour) return false;
364                if (minute != other.minute) return false;
365                if (month != other.month) return false;
366                if (second != other.second) return false;
367                if (utc != other.utc) return false;
368                if (year != other.year) return false;
369                return true;
370        }
371
372        public int compareTo(DateTimeComponents that) {
373                int c = this.year - that.year;
374                if (c != 0) {
375                        return c;
376                }
377
378                c = this.month - that.month;
379                if (c != 0) {
380                        return c;
381                }
382
383                c = this.date - that.date;
384                if (c != 0) {
385                        return c;
386                }
387
388                c = this.hour - that.hour;
389                if (c != 0) {
390                        return c;
391                }
392
393                c = this.minute - that.minute;
394                if (c != 0) {
395                        return c;
396                }
397
398                c = this.second - that.second;
399                if (c != 0) {
400                        return c;
401                }
402
403                return 0;
404        }
405
406        public boolean before(DateTimeComponents that) {
407                return this.compareTo(that) < 0;
408        }
409
410        public boolean after(DateTimeComponents that) {
411                return this.compareTo(that) > 0;
412        }
413}