001package biweekly.util;
002
003import java.util.TimeZone;
004import java.util.regex.Matcher;
005import java.util.regex.Pattern;
006
007/*
008 Copyright (c) 2013-2015, Michael Angstadt
009 All rights reserved.
010
011 Redistribution and use in source and binary forms, with or without
012 modification, are permitted provided that the following conditions are met: 
013
014 1. Redistributions of source code must retain the above copyright notice, this
015 list of conditions and the following disclaimer. 
016 2. Redistributions in binary form must reproduce the above copyright notice,
017 this list of conditions and the following disclaimer in the documentation
018 and/or other materials provided with the distribution. 
019
020 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
021 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
022 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
023 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
024 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
025 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
026 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
027 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
028 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
029 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
030 */
031
032/**
033 * Represents a UTC offset.
034 * @author Michael Angstadt
035 */
036public final class UtcOffset {
037        private final long millis;
038
039        /**
040         * @param positive true if the offset is positive, false if it is negative
041         * @param hour the hour component of the offset (the sign of this integer is
042         * ignored)
043         * @param minute the minute component of the offset (the sign of this
044         * integer is ignored)
045         */
046        public UtcOffset(boolean positive, int hour, int minute) {
047                //Note: The (hour, minute) constructor was removed because it could not handle timezones such as "-0030"
048                int sign = positive ? 1 : -1;
049                hour = Math.abs(hour);
050                minute = Math.abs(minute);
051
052                millis = sign * (hoursToMillis(hour) + minutesToMillis(minute));
053        }
054
055        /**
056         * @param millis the offset in milliseconds
057         */
058        public UtcOffset(long millis) {
059                this.millis = millis;
060        }
061
062        /**
063         * Parses a UTC offset from a string.
064         * @param text the text to parse (e.g. "-0500")
065         * @return the parsed UTC offset
066         * @throws IllegalArgumentException if the text cannot be parsed
067         */
068        public static UtcOffset parse(String text) {
069                Pattern timeZoneRegex = Pattern.compile("^([-\\+])?(\\d{1,2})(:?(\\d{2}))?(:?(\\d{2}))?$");
070                Matcher m = timeZoneRegex.matcher(text);
071
072                if (!m.find()) {
073                        throw new IllegalArgumentException("Offset string is not in ISO8610 format: " + text);
074                }
075
076                String signStr = m.group(1);
077                boolean positive = !"-".equals(signStr);
078
079                String hourStr = m.group(2);
080                int hourOffset = Integer.parseInt(hourStr);
081
082                String minuteStr = m.group(4);
083                int minuteOffset = (minuteStr == null) ? 0 : Integer.parseInt(minuteStr);
084
085                return new UtcOffset(positive, hourOffset, minuteOffset);
086        }
087
088        /**
089         * Creates a UTC offset from a {@link TimeZone} object.
090         * @param timezone the timezone
091         * @return the UTC offset
092         */
093        public static UtcOffset parse(TimeZone timezone) {
094                long offset = timezone.getOffset(System.currentTimeMillis());
095                return new UtcOffset(offset);
096        }
097
098        /**
099         * Gets the offset in milliseconds.
100         * @return the offset in milliseconds
101         */
102        public long getMillis() {
103                return millis;
104        }
105
106        /**
107         * Converts this offset to its ISO string representation using "basic"
108         * format.
109         * @return the ISO string representation (e.g. "-0500")
110         */
111        @Override
112        public String toString() {
113                return toString(false);
114        }
115
116        /**
117         * Converts this offset to its ISO string representation.
118         * @param extended true to use extended format (e.g. "-05:00"), false to use
119         * basic format (e.g. "-0500")
120         * @return the ISO string representation
121         */
122        public String toString(boolean extended) {
123                StringBuilder sb = new StringBuilder();
124
125                boolean positive = (millis >= 0);
126                long hour = Math.abs(millisToHours(millis));
127                long minute = Math.abs(millisToMinutes(millis));
128
129                sb.append(positive ? '+' : '-');
130
131                if (hour < 10) {
132                        sb.append('0');
133                }
134                sb.append(hour);
135
136                if (extended) {
137                        sb.append(':');
138                }
139
140                if (minute < 10) {
141                        sb.append('0');
142                }
143                sb.append(minute);
144
145                return sb.toString();
146        }
147
148        @Override
149        public int hashCode() {
150                final int prime = 31;
151                int result = 1;
152                result = prime * result + (int) (millis ^ (millis >>> 32));
153                return result;
154        }
155
156        @Override
157        public boolean equals(Object obj) {
158                if (this == obj) return true;
159                if (obj == null) return false;
160                if (getClass() != obj.getClass()) return false;
161                UtcOffset other = (UtcOffset) obj;
162                if (millis != other.millis) return false;
163                return true;
164        }
165
166        private static long hoursToMillis(long hours) {
167                return hours * 60 * 60 * 1000;
168        }
169
170        private static long minutesToMillis(long minutes) {
171                return minutes * 60 * 1000;
172        }
173
174        private static long millisToHours(long millis) {
175                return millis / 1000 / 60 / 60;
176        }
177
178        private static long millisToMinutes(long millis) {
179                return (millis / 1000 / 60) % 60;
180        }
181}