001package ezvcard.util;
002
003import java.util.TimeZone;
004
005import ezvcard.Messages;
006
007/*
008 Copyright (c) 2012-2018, 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                /*
048                 * Note: The (hour, minute) constructor was removed because it could not
049                 * handle timezones such as "-0030".
050                 */
051                int sign = positive ? 1 : -1;
052                hour = Math.abs(hour);
053                minute = Math.abs(minute);
054
055                millis = sign * (hoursToMillis(hour) + minutesToMillis(minute));
056        }
057
058        /**
059         * @param millis the offset in milliseconds
060         */
061        public UtcOffset(long millis) {
062                this.millis = millis;
063        }
064
065        /**
066         * Parses a UTC offset from a string.
067         * @param text the text to parse (e.g. "-0500")
068         * @return the parsed UTC offset
069         * @throws IllegalArgumentException if the text cannot be parsed
070         */
071        public static UtcOffset parse(String text) {
072                int i = 0;
073                char sign = text.charAt(i);
074                boolean positive = true;
075                if (sign == '-') {
076                        positive = false;
077                        i++;
078                } else if (sign == '+') {
079                        i++;
080                }
081
082                int maxLength = i + 4;
083                int colon = text.indexOf(':', i);
084                if (colon >= 0) {
085                        maxLength++;
086                }
087                if (text.length() > maxLength) {
088                        throw Messages.INSTANCE.getIllegalArgumentException(40, text);
089                }
090
091                String hourStr, minuteStr = null;
092                if (colon < 0) {
093                        hourStr = text.substring(i);
094                        int minutePos = hourStr.length() - 2;
095                        if (minutePos > 0) {
096                                minuteStr = hourStr.substring(minutePos);
097                                hourStr = hourStr.substring(0, minutePos);
098                        }
099                } else {
100                        hourStr = text.substring(i, colon);
101                        if (colon < text.length() - 1) {
102                                minuteStr = text.substring(colon + 1);
103                        }
104                }
105
106                int hour, minute;
107                try {
108                        hour = Integer.parseInt(hourStr);
109                        minute = (minuteStr == null) ? 0 : Integer.parseInt(minuteStr);
110                } catch (NumberFormatException e) {
111                        throw Messages.INSTANCE.getIllegalArgumentException(40, text);
112                }
113
114                return new UtcOffset(positive, hour, minute);
115        }
116
117        /**
118         * Creates a UTC offset from a {@link TimeZone} object.
119         * @param timezone the timezone
120         * @return the UTC offset
121         */
122        public static UtcOffset parse(TimeZone timezone) {
123                long offset = timezone.getOffset(System.currentTimeMillis());
124                return new UtcOffset(offset);
125        }
126
127        /**
128         * Gets the offset in milliseconds.
129         * @return the offset in milliseconds
130         */
131        public long getMillis() {
132                return millis;
133        }
134
135        /**
136         * Converts this offset to its ISO string representation using "basic"
137         * format.
138         * @return the ISO string representation (e.g. "-0500")
139         */
140        @Override
141        public String toString() {
142                return toString(false);
143        }
144
145        /**
146         * Converts this offset to its ISO string representation.
147         * @param extended true to use extended format (e.g. "-05:00"), false to use
148         * basic format (e.g. "-0500")
149         * @return the ISO string representation
150         */
151        public String toString(boolean extended) {
152                StringBuilder sb = new StringBuilder();
153
154                boolean positive = (millis >= 0);
155                long hour = Math.abs(millisToHours(millis));
156                long minute = Math.abs(millisToMinutes(millis));
157
158                sb.append(positive ? '+' : '-');
159
160                if (hour < 10) {
161                        sb.append('0');
162                }
163                sb.append(hour);
164
165                if (extended) {
166                        sb.append(':');
167                }
168
169                if (minute < 10) {
170                        sb.append('0');
171                }
172                sb.append(minute);
173
174                return sb.toString();
175        }
176
177        @Override
178        public int hashCode() {
179                final int prime = 31;
180                int result = 1;
181                result = prime * result + (int) (millis ^ (millis >>> 32));
182                return result;
183        }
184
185        @Override
186        public boolean equals(Object obj) {
187                if (this == obj) return true;
188                if (obj == null) return false;
189                if (getClass() != obj.getClass()) return false;
190                UtcOffset other = (UtcOffset) obj;
191                if (millis != other.millis) return false;
192                return true;
193        }
194
195        private static long hoursToMillis(long hours) {
196                return hours * 60 * 60 * 1000;
197        }
198
199        private static long minutesToMillis(long minutes) {
200                return minutes * 60 * 1000;
201        }
202
203        private static long millisToHours(long millis) {
204                return millis / 1000 / 60 / 60;
205        }
206
207        private static long millisToMinutes(long millis) {
208                return (millis / 1000 / 60) % 60;
209        }
210}